// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of csslib.parser;

/**
 * CSS polyfill emits CSS to be understood by older parsers that which do not
 * understand (var, calc, etc.).
 */
class PolyFill {
  final Messages _messages;
  Map<String, VarDefinition> _allVarDefinitions =
      new Map<String, VarDefinition>();

  Set<StyleSheet> allStyleSheets = new Set<StyleSheet>();

  /**
   * [_pseudoElements] list of known pseudo attributes found in HTML, any
   * CSS pseudo-elements 'name::custom-element' is mapped to the manged name
   * associated with the pseudo-element key.
   */
  PolyFill(this._messages);

  /**
   * Run the analyzer on every file that is a style sheet or any component that
   * has a style tag.
   */
  void process(StyleSheet styleSheet, {List<StyleSheet> includes: null}) {
    if (includes != null) {
      processVarDefinitions(includes);
    }
    processVars(styleSheet);

    // Remove all var definitions for this style sheet.
    new _RemoveVarDefinitions().visitTree(styleSheet);
  }

  /** Process all includes looking for var definitions. */
  void processVarDefinitions(List<StyleSheet> includes) {
    for (var include in includes) {
      _allVarDefinitions = (new _VarDefinitionsIncludes(_allVarDefinitions)
            ..visitTree(include))
          .varDefs;
    }
  }

  void processVars(StyleSheet styleSheet) {
    // Build list of all var definitions.
    var mainStyleSheetVarDefs =
        (new _VarDefAndUsage(this._messages, _allVarDefinitions)
              ..visitTree(styleSheet))
            .varDefs;

    // Resolve all definitions to a non-VarUsage (terminal expression).
    mainStyleSheetVarDefs.forEach((key, value) {
      for (var _ in (value.expression as Expressions).expressions) {
        mainStyleSheetVarDefs[key] =
            _findTerminalVarDefinition(_allVarDefinitions, value);
      }
    });
  }
}

/** Build list of all var definitions in all includes. */
class _VarDefinitionsIncludes extends Visitor {
  final Map<String, VarDefinition> varDefs;

  _VarDefinitionsIncludes(this.varDefs);

  void visitTree(StyleSheet tree) {
    visitStyleSheet(tree);
  }

  visitVarDefinition(VarDefinition node) {
    // Replace with latest variable definition.
    varDefs[node.definedName] = node;
    super.visitVarDefinition(node);
  }

  void visitVarDefinitionDirective(VarDefinitionDirective node) {
    visitVarDefinition(node.def);
  }
}

/**
 * Find var- definitions in a style sheet.
 * [found] list of known definitions.
 */
class _VarDefAndUsage extends Visitor {
  final Messages _messages;
  final Map<String, VarDefinition> _knownVarDefs;
  final Map<String, VarDefinition> varDefs = new Map<String, VarDefinition>();

  VarDefinition currVarDefinition;
  List<Expression> currentExpressions;

  _VarDefAndUsage(this._messages, this._knownVarDefs);

  void visitTree(StyleSheet tree) {
    visitStyleSheet(tree);
  }

  visitVarDefinition(VarDefinition node) {
    // Replace with latest variable definition.
    currVarDefinition = node;

    _knownVarDefs[node.definedName] = node;
    varDefs[node.definedName] = node;

    super.visitVarDefinition(node);

    currVarDefinition = null;
  }

  void visitVarDefinitionDirective(VarDefinitionDirective node) {
    visitVarDefinition(node.def);
  }

  void visitExpressions(Expressions node) {
    currentExpressions = node.expressions;
    super.visitExpressions(node);
    currentExpressions = null;
  }

  void visitVarUsage(VarUsage node) {
    if (currVarDefinition != null && currVarDefinition.badUsage) return;

    // Don't process other var() inside of a varUsage.  That implies that the
    // default is a var() too.  Also, don't process any var() inside of a
    // varDefinition (they're just place holders until we've resolved all real
    // usages.
    var expressions = currentExpressions;
    var index = expressions.indexOf(node);
    assert(index >= 0);
    var def = _knownVarDefs[node.name];
    if (def != null) {
      if (def.badUsage) {
        // Remove any expressions pointing to a bad var definition.
        expressions.removeAt(index);
        return;
      }
      _resolveVarUsage(currentExpressions, index,
          _findTerminalVarDefinition(_knownVarDefs, def));
    } else if (node.defaultValues.any((e) => e is VarUsage)) {
      // Don't have a VarDefinition need to use default values resolve all
      // default values.
      var terminalDefaults = <Expression>[];
      for (var defaultValue in node.defaultValues) {
        terminalDefaults.addAll(resolveUsageTerminal(defaultValue));
      }
      expressions.replaceRange(index, index + 1, terminalDefaults);
    } else if (node.defaultValues.isNotEmpty) {
      // No VarDefinition but default value is a terminal expression; use it.
      expressions.replaceRange(index, index + 1, node.defaultValues);
    } else {
      if (currVarDefinition != null) {
        currVarDefinition.badUsage = true;
        var mainStyleSheetDef = varDefs[node.name];
        if (mainStyleSheetDef != null) {
          varDefs.remove(currVarDefinition.property);
        }
      }
      // Remove var usage that points at an undefined definition.
      expressions.removeAt(index);
      _messages.warning("Variable is not defined.", node.span);
    }

    var oldExpressions = currentExpressions;
    currentExpressions = node.defaultValues;
    super.visitVarUsage(node);
    currentExpressions = oldExpressions;
  }

  List<Expression> resolveUsageTerminal(VarUsage usage) {
    var result = <Expression>[];

    var varDef = _knownVarDefs[usage.name];
    var expressions;
    if (varDef == null) {
      // VarDefinition not found try the defaultValues.
      expressions = usage.defaultValues;
    } else {
      // Use the VarDefinition found.
      expressions = (varDef.expression as Expressions).expressions;
    }

    for (var expr in expressions) {
      if (expr is VarUsage) {
        // Get terminal value.
        result.addAll(resolveUsageTerminal(expr));
      }
    }

    // We're at a terminal just return the VarDefinition expression.
    if (result.isEmpty && varDef != null) {
      result = (varDef.expression as Expressions).expressions;
    }

    return result;
  }

  _resolveVarUsage(List<Expression> expressions, int index, VarDefinition def) {
    var defExpressions = (def.expression as Expressions).expressions;
    expressions.replaceRange(index, index + 1, defExpressions);
  }
}

/** Remove all var definitions. */
class _RemoveVarDefinitions extends Visitor {
  void visitTree(StyleSheet tree) {
    visitStyleSheet(tree);
  }

  void visitStyleSheet(StyleSheet ss) {
    ss.topLevels.removeWhere((e) => e is VarDefinitionDirective);
    super.visitStyleSheet(ss);
  }

  void visitDeclarationGroup(DeclarationGroup node) {
    node.declarations.removeWhere((e) => e is VarDefinition);
    super.visitDeclarationGroup(node);
  }
}

/** Find terminal definition (non VarUsage implies real CSS value). */
VarDefinition _findTerminalVarDefinition(
    Map<String, VarDefinition> varDefs, VarDefinition varDef) {
  var expressions = varDef.expression as Expressions;
  for (var expr in expressions.expressions) {
    if (expr is VarUsage) {
      var usageName = expr.name;
      var foundDef = varDefs[usageName];

      // If foundDef is unknown check if defaultValues; if it exist then resolve
      // to terminal value.
      if (foundDef == null) {
        // We're either a VarUsage or terminal definition if in varDefs;
        // either way replace VarUsage with it's default value because the
        // VarDefinition isn't found.
        var defaultValues = expr.defaultValues;
        var replaceExprs = expressions.expressions;
        assert(replaceExprs.length == 1);
        replaceExprs.replaceRange(0, 1, defaultValues);
        return varDef;
      }
      if (foundDef is VarDefinition) {
        return _findTerminalVarDefinition(varDefs, foundDef);
      }
    } else {
      // Return real CSS property.
      return varDef;
    }
  }

  // Didn't point to a var definition that existed.
  return varDef;
}
