// Copyright 2016 Workiva Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

library over_react.transformer;

import 'dart:async';

import 'package:analyzer/analyzer.dart';
import 'package:barback/barback.dart';
import 'package:over_react/src/transformer/declaration_parsing.dart';
import 'package:over_react/src/transformer/impl_generation.dart';
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';
import 'package:transformer_utils/transformer_utils.dart';

/// A transformer that modifies `.dart` files, aiding the declaration of React components
/// using the `@Factory()`, `@Props()` `@Component()`, etc. annotations.
///
/// This transformer:
///
/// * Generates prop/state accessors.
/// * Generates implementations for stubbed props/state/component classes.
/// * Creates component factories, registers them with react-dart, and wires them up to
/// their associated props/component implementations.
class WebSkinDartTransformer extends Transformer implements LazyTransformer {
  final BarbackSettings _settings;

  WebSkinDartTransformer.asPlugin(this._settings);

  /// Declare the assets this transformer uses. Only dart assets will be transformed.
  @override
  String get allowedExtensions => ".dart";

  @override
  void declareOutputs(DeclaringTransform transform) {
    transform.declareOutput(transform.primaryId);
    transform.consumePrimary();

    if (_settings.mode == BarbackMode.DEBUG) {
      transform.declareOutput(transform.primaryId.addExtension('.diff.html'));
    }
  }

  /// Converts [id] to a "package:" URI.
  ///
  /// This will return a schemeless URI if [id] doesn't represent a library in
  /// `lib/`.
  static Uri idToPackageUri(AssetId id) {
    if (!id.path.startsWith('lib/')) {
      return new Uri(path: id.path);
    }

    return new Uri(scheme: 'package',
        path: p.url.join(id.package, id.path.replaceFirst('lib/', '')));
  }

  @override
  Future apply(Transform transform) async {
    var primaryInputContents = await transform.primaryInput.readAsString();

    SourceFile sourceFile = new SourceFile(primaryInputContents, url: idToPackageUri(transform.primaryInput.id));
    TransformedSourceFile transformedFile = new TransformedSourceFile(sourceFile);
    TransformLogger logger = new JetBrainsFriendlyLogger(transform.logger);

    // If the source file might contain annotations that necessitate generation,
    // parse the declarations and generate code.
    // If not, don't skip this step to avoid parsing files that definitely won't generate anything.
    if (ParsedDeclarations.mightContainDeclarations(primaryInputContents)) {
      // Parse the source file on its own and use the resultant AST to...
      var unit = parseCompilationUnit(primaryInputContents,
        suppressErrors: true,
        name: transform.primaryInput.id.path,
        parseFunctionBodies: false
      );

      ParsedDeclarations declarations = new ParsedDeclarations(unit, sourceFile, logger);

      // If there are no errors, generate the component.
      if (!declarations.hasErrors) {
        new ImplGenerator(logger, transformedFile)
            .generate(declarations);
      }
    }

    // Replace static $PropKeys instantiations with prop keys
    if (new RegExp(r'\$PropKeys').hasMatch(primaryInputContents)) {
      var propKeysPattern = new RegExp(r'(?:const|new)\s+\$PropKeys\s*\(\s*([\$A-Za-z0-9_\.]+)\s*\)');
      propKeysPattern.allMatches(sourceFile.getText(0)).forEach((match) {
        var symbolName = match.group(1);

        var replacement = '$symbolName.${ImplGenerator.staticPropKeysName} /* GENERATED from \$PropKeys usage */';

        transformedFile.replace(sourceFile.span(match.start, match.end), replacement);
      });
    }

    // Replace static $Props instantiations with props
    if (new RegExp(r'\$Props').hasMatch(primaryInputContents)) {
      var propKeysPattern = new RegExp(r'(?:const|new)\s+\$Props\s*\(\s*([\$A-Za-z0-9_\.]+)\s*\)');
      propKeysPattern.allMatches(sourceFile.getText(0)).forEach((match) {
        var symbolName = match.group(1);

        var replacement = '$symbolName.${ImplGenerator.staticConsumedPropsName} /* GENERATED from \$Props usage */';

        transformedFile.replace(sourceFile.span(match.start, match.end), replacement);
      });
    }

    if (transformedFile.isModified) {
      // Output the transformed source.
      transform.addOutput(new Asset.fromString(transform.primaryInput.id, transformedFile.getTransformedText()));
    } else {
      // Output the unmodified input.
      transform.addOutput(transform.primaryInput);
    }

    if (_settings.mode == BarbackMode.DEBUG) {
      transform.addOutput(new Asset.fromString(transform.primaryInput.id.addExtension('.diff.html'),
          transformedFile.getHtmlDiff()
      ));
    }
  }
}