import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:jael3/jael3.dart' as jael;
import 'package:jael3_preprocessor/jael3_preprocessor.dart' as jael;
import 'package:jael3_web/jael3_web.dart';
import 'package:path/path.dart' as p;
import 'package:source_gen/source_gen.dart';
import 'util.dart';

var _upper = RegExp(r'^[A-Z]');

Builder jaelComponentBuilder(_) {
  return SharedPartBuilder([JaelComponentGenerator()], 'jael_web_cmp');
}

class JaelComponentGenerator extends GeneratorForAnnotation<Jael> {
  @override
  Future<String> generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) async {
    if (element is ClassElement) {
      // Load the template
      String? templateString;
      var inputId = buildStep.inputId;
      var ann = Jael(
        template: annotation.peek('template')?.stringValue,
        templateUrl: annotation.peek('templateUrl')?.stringValue,
        asDsx: annotation.peek('asDsx')?.boolValue ?? false,
      );

      if (ann.template == null && ann.templateUrl == null) {
        throw 'Both `template` and `templateUrl` cannot be null.';
      }

      if (ann.template != null) {
        templateString = ann.template;
      } else {
        var dir = p.dirname(inputId.path);
        var assetId = AssetId(inputId.package, p.join(dir, ann.templateUrl));
        if (!await buildStep.canRead(assetId)) {
          throw 'Cannot find template "${assetId.uri}"';
        } else {
          templateString = await buildStep.readAsString(assetId);
        }
      }

      var fs = BuildFileSystem(buildStep, inputId.package);
      var errors = <jael.JaelError>[];
      var doc = jael.parseDocument(templateString!,
          sourceUrl: inputId.uri, asDSX: ann.asDsx!, onError: errors.add);
      if (errors.isEmpty) {
        doc = await jael.resolve(doc!, fs.file(inputId.uri).parent,
            onError: errors.add);
      }

      if (errors.isNotEmpty) {
        errors.forEach(log.severe);
        throw 'Jael processing finished with ${errors.length} error(s).';
      }

      // Generate a _XJaelTemplate mixin class
      var clazz = Class((b) {
        b
          ..abstract = true
          ..name = '_${element.name}JaelTemplate'
          ..implements.add(convertTypeReference(element.supertype));

        // Add fields corresponding to each of the class's fields.
        for (var field in element.fields) {
          b.methods.add(Method((b) {
            b
              ..name = field.name
              ..type = MethodType.getter
              ..returns = convertTypeReference(field.type);
          }));
        }

        // ... And methods too.
        for (var method in element.methods) {
          b.methods.add(Method((b) {
            b
              ..name = method.name
              ..returns = convertTypeReference(method.returnType)
              ..requiredParameters.addAll(method.parameters
                  .where(isRequiredParameter)
                  .map(convertParameter))
              ..optionalParameters.addAll(method.parameters
                  .where(isOptionalParameter)
                  .map(convertParameter));
          }));
        }

        // Add a render() stub
        b.methods.add(Method((b) {
          b
            ..name = 'render'
            ..returns = refer('DomNode')
            ..annotations.add(refer('override'))
            ..body = Block((b) {
              var result = compileElementChild(doc!.root);
              b.addExpression(result.returned);
            });
        }));
      });

      return clazz.accept(DartEmitter()).toString();
    } else {
      throw '@Jael() is only supported for classes.';
    }
  }

  Expression compileElementChild(jael.ElementChild child) {
    if (child is jael.TextNode || child is jael.Text) {
      return refer('text').call([literalString(child.span.text)]);
    } else if (child is jael.Interpolation) {
      Expression expr = CodeExpression(Code(child.expression.span.text));
      expr = expr.property('toString').call([]);
      return refer('text').call([expr]);
    } else if (child is jael.Element) {
      // TODO: Handle strict resolution
      var attrs = <String, Expression>{};
      for (var attr in child.attributes) {
        attrs[attr.name] = attr.value == null
            ? literalTrue
            : CodeExpression(Code(attr.value!.span.text));
      }

      var tagName = child.tagName.name;
      if (!_upper.hasMatch(tagName)) {
        return refer('h').call([
          literalString(tagName),
          literalMap(attrs),
          literalList(child.children.map(compileElementChild)),
        ]);
      } else {
        // TODO: How to pass children?
        return refer(tagName).newInstance([], attrs);
      }
      // return refer(child.tagName.name).newInstance([]);
    } else {
      throw 'Unsupported: $child';
    }
  }
}