# frozen_string_literal: true

# Copyright Andreas Alin <andreas.alin@gmail.com>
# Released under AGPL-3.0

require "base64"
require "digest/sha2"
require "mayu/css"
require "syntax_tree"
require_relative "../../../style_sheet"
require_relative "frozen_string_literal_visitor"

module Mayu
  module Modules
    module Loaders
      module Transformers
        class CSS
          include SyntaxTree::DSL

          def self.transform(source_path, source)
            Mayu::CSS
              .transform(source_path, source)
              .then do
                new(source_path, _1).build_inline_ast(assign_default: true)
              end
              .accept(FrozenStringLiteralVisitor.new)
              .then { SyntaxTree::Formatter.format("", _1).rstrip + "\n" }
          end

          def self.transform_inline(source_path, source, **options)
            Mayu::CSS
              .transform(source_path, source)
              .then { new(source_path, _1, **options).build_inline_ast }
          end

          def initialize(
            source_path,
            parse_result,
            dependency_const_prefix: "Dep_",
            code_const_name: "CODE",
            content_hash_const_name: "CONTENT_HASH"
          )
            @source_path = source_path
            @parse_result = parse_result
            @dependency_const_prefix = dependency_const_prefix
            @code_const_name = code_const_name
            @content_hash_const_name = content_hash_const_name
          end

          def build_inline_ast(assign_default: false)
            new_style_sheet =
              ARef(
                ConstPathRef(VarRef(Const("Mayu")), Const("StyleSheet")),
                Args(
                  [
                    BareAssocHash(
                      [
                        Assoc(
                          Label("source_filename:"),
                          StringLiteral(
                            [TStringContent(@source_path.delete_prefix("/"))],
                            '"'
                          )
                        ),
                        Assoc(
                          Label("content_hash:"),
                          build_content_hash_string
                        ),
                        Assoc(Label("classes:"), build_classes_hash),
                        Assoc(Label("content:"), build_code_heredoc)
                      ]
                    )
                  ]
                )
              )

            Statements(
              [
                *build_imports,
                if assign_default
                  [
                    Assign(VarField(Const("Default")), VarRef(new_style_sheet)),
                    build_assets_code
                  ]
                else
                  new_style_sheet
                end
              ].flatten.compact
            )
          end

          private

          def build_imports
            @parse_result.dependencies.map do |dep|
              dep => { placeholder:, url: }

              Assign(
                VarField(placeholder_const(placeholder)),
                build_import(url)
              )
            end
          end

          def placeholder_const(placeholder)
            Const(@dependency_const_prefix + placeholder.tr("-", "_"))
          end

          def build_import(url)
            Command(
              Ident("import"),
              Args([StringLiteral([TStringContent(url)], '"')]),
              nil
            )
          end

          def build_content_hash_string
            StringLiteral(
              [
                TStringContent(
                  @parse_result
                    .code
                    .then { Digest::SHA256.digest(_1) }
                    .then { Base64.urlsafe_encode64(_1, padding: false) }
                )
              ],
              '"'
            )
          end

          def build_classes_hash
            HashLiteral(LBrace("{"), build_classes_assocs)
          end

          def build_classes_assocs
            {
              **@parse_result.classes,
              **@parse_result.elements.transform_keys { "__#{_1}" }
            }.transform_keys(&:to_s)
              .sort_by(&:first)
              .map do |key, value|
                Assoc(
                  if key.match(/\A[A-Za-z0-9_]\z/)
                    Label("#{key}:")
                  else
                    DynaSymbol([TStringContent(key)], '"')
                  end,
                  StringLiteral(
                    [
                      TStringContent(
                        join_class(
                          value.to_s,
                          @parse_result.exports,
                          @parse_result.classes
                        )
                      )
                    ],
                    '"'
                  )
                )
              end
          end

          def join_class(klass, exports, classes)
            if composes = exports[klass]&.composes
              [
                klass,
                *composes.map do |compose|
                  case compose
                  in Mayu::CSS::ComposeLocal
                    classes[compose.name.to_sym]
                  end
                end
              ].join(" ")
            else
              klass
            end
          end

          def build_code_heredoc
            Heredoc(
              HeredocBeg("<<CSS"),
              HeredocEnd("CSS"),
              nil,
              build_code_heredoc_inner
            )
          end

          def build_code_heredoc_inner
            parts = []
            remains = @parse_result.code.gsub("\\", "\\\\\\\\") + "\n"

            @parse_result.dependencies.map do |dep|
              dep => { placeholder: }
              remains.split(placeholder, 2) => [part, remains]

              parts.push(
                TStringContent(part),
                StringEmbExpr(
                  Statements(
                    [
                      CallNode(
                        ConstPathRef(
                          VarRef(Const("Mayu")),
                          Const("StyleSheet")
                        ),
                        Period("."),
                        Ident("encode_url"),
                        ArgParen(
                          Args(
                            [
                              CallNode(
                                VarRef(placeholder_const(placeholder)),
                                Period("."),
                                Ident("to_s"),
                                nil
                              )
                            ]
                          )
                        )
                      )
                    ]
                  )
                )
              )
            end

            parts.push(TStringContent(remains)) unless remains.empty?

            parts
          end

          def build_assets_code
            CallNode(
              nil,
              nil,
              Ident("add_asset"),
              ArgParen(
                Args(
                  [
                    ARef(
                      ConstPathRef(
                        ConstPathRef(
                          ConstPathRef(VarRef(Const("Mayu")), Const("Assets")),
                          Const("Generators")
                        ),
                        Const("Text")
                      ),
                      Args(
                        [
                          CallNode(
                            VarField(Const("Default")),
                            Period("."),
                            Ident("filename"),
                            nil
                          ),
                          CallNode(
                            VarField(Const("Default")),
                            Period("."),
                            Ident("content"),
                            nil
                          )
                        ]
                      )
                    )
                  ]
                )
              )
            )
          end
        end
      end
    end
  end
end
