/*
 * Copyright 2008 Google 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.
 */
package com.google.gwt.resources.css;

import com.google.gwt.dev.util.TextOutput;
import com.google.gwt.resources.css.ast.Context;
import com.google.gwt.resources.css.ast.CssDef;
import com.google.gwt.resources.css.ast.CssEval;
import com.google.gwt.resources.css.ast.CssExternalSelectors;
import com.google.gwt.resources.css.ast.CssFontFace;
import com.google.gwt.resources.css.ast.CssIf;
import com.google.gwt.resources.css.ast.CssMediaRule;
import com.google.gwt.resources.css.ast.CssNoFlip;
import com.google.gwt.resources.css.ast.CssNode;
import com.google.gwt.resources.css.ast.CssPageRule;
import com.google.gwt.resources.css.ast.CssProperty;
import com.google.gwt.resources.css.ast.CssRule;
import com.google.gwt.resources.css.ast.CssSelector;
import com.google.gwt.resources.css.ast.CssSprite;
import com.google.gwt.resources.css.ast.CssSubstitution;
import com.google.gwt.resources.css.ast.CssUnknownAtRule;
import com.google.gwt.resources.css.ast.CssUrl;
import com.google.gwt.resources.css.ast.CssVisitor;

import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Generates a static CSS template string and provides information on where to
 * inject dynamic expressions.
 */
public class CssGenerationVisitor extends CssVisitor {
  private boolean needsOpenBrace;
  private boolean needsComma;

  private final TextOutput out;

  private final boolean substituteDots;
  private final SortedMap<Integer, List<CssSubstitution>> substitutionPositions = new TreeMap<Integer, List<CssSubstitution>>();

  /**
   * Constructor.
   * 
   * @param out the output hondler
   */
  public CssGenerationVisitor(TextOutput out) {
    this(out, false);
  }

  /**
   * Constructor for producing an abbreviated form of the template for use with
   * {@link CssNode#toString()}.
   * 
   * @param out the output handler
   * @param substituteDots if <code>true</code> locations in the text output
   *          where expression substitutions would normally occur are replaced
   *          with a textual placeholder
   */
  public CssGenerationVisitor(TextOutput out, boolean substituteDots) {
    this.out = out;
    this.substituteDots = substituteDots;
  }

  @Override
  public void endVisit(CssFontFace x, Context ctx) {
    closeBrace();
  }

  @Override
  public void endVisit(CssIf x, Context ctx) {
    // Match up an explanatory comment
    out.indentOut();
    out.printOpt("/* } */");
    out.newlineOpt();
  }

  @Override
  public void endVisit(CssMediaRule x, Context ctx) {
    out.indentOut();
    out.print("}");
    out.newlineOpt();
  }

  @Override
  public void endVisit(CssNoFlip x, Context ctx) {
    out.printOpt("/*} @noflip */");
    out.newlineOpt();
  }

  @Override
  public void endVisit(CssPageRule x, Context ctx) {
    out.indentOut();
    out.print("}");
    out.newlineOpt();
  }

  @Override
  public void endVisit(CssRule x, Context ctx) {
    if (!x.getProperties().isEmpty()) {
      // Don't print empty rule blocks
      closeBrace();
    }
  }

  @Override
  public void endVisit(CssUnknownAtRule x, Context ctx) {
    out.printOpt("/* Unknown at-rule */\n");
    out.print(x.getRule());
  }

  public SortedMap<Integer, List<CssSubstitution>> getSubstitutionPositions() {
    return substitutionPositions;
  }

  @Override
  public boolean visit(CssFontFace x, Context ctx) {
    out.print("@font-face");
    openBrace();
    return true;
  }

  @Override
  public boolean visit(CssDef x, Context ctx) {
    // These are not valid CSS
    out.printOpt("/* CssDef */");
    out.newlineOpt();
    return false;
  }

  @Override
  public boolean visit(CssEval x, Context ctx) {
    // These are not valid CSS
    out.printOpt("/* CssEval */");
    out.newlineOpt();
    return false;
  }

  @Override
  public boolean visit(CssExternalSelectors x, Context ctx) {
    // These are not valid CSS
    out.printOpt("/* @external");
    for (String className : x.getClasses()) {
      out.printOpt(" ");
      out.printOpt(className);
    }
    out.printOpt("; */");
    out.newlineOpt();
    return false;
  }

  @Override
  public boolean visit(CssIf x, Context ctx) {
    // Record where the contents of the if block should be inserted
    StringBuilder expr = new StringBuilder("/* @if ");
    if (x.getExpression() != null) {
      expr.append(x.getExpression()).append(" ");
    } else {
      expr.append(x.getPropertyName()).append(" ");
      for (String v : x.getPropertyValues()) {
        expr.append(v).append(" ");
      }
    }
    expr.append("{ */");
    out.printOpt(expr.toString());
    out.newlineOpt();
    out.indentIn();
    addSubstitition(x);
    return false;
  }

  @Override
  public boolean visit(CssMediaRule x, Context ctx) {
    out.print("@media");
    boolean isFirst = true;
    for (String m : x.getMedias()) {
      if (isFirst) {
        out.print(" ");
        isFirst = false;
      } else {
        comma();
      }
      out.print(m);
    }
    spaceOpt();
    out.print("{");
    out.newlineOpt();
    out.indentIn();
    return true;
  }

  @Override
  public boolean visit(CssNoFlip x, Context ctx) {
    out.printOpt("/*@noflip { */");
    out.newlineOpt();
    return true;
  }

  @Override
  public boolean visit(CssPageRule x, Context ctx) {
    out.print("@page");
    if (x.getPseudoPage() != null) {
      out.print(" :");
      out.print(x.getPseudoPage());
    }
    spaceOpt();
    out.print("{");
    out.newlineOpt();
    out.indentIn();
    return true;
  }

  @Override
  public boolean visit(CssProperty x, Context ctx) {
    if (needsOpenBrace) {
      openBrace();
      needsOpenBrace = false;
    }

    out.print(x.getName());
    colon();
    addSubstitition(x);

    if (x.isImportant()) {
      important();
    }

    semi();

    return true;
  }

  @Override
  public boolean visit(CssRule x, Context ctx) {
    if (x.getProperties().isEmpty()) {
      // Don't print empty rule blocks
      return false;
    }

    needsOpenBrace = true;
    needsComma = false;
    return true;
  }

  @Override
  public boolean visit(CssSelector x, Context ctx) {
    if (needsComma) {
      comma();
    }
    needsComma = true;
    out.print(x.getSelector());
    return true;
  }

  @Override
  public boolean visit(CssSprite x, Context ctx) {
    // These are not valid CSS
    out.printOpt("/* CssSprite */");
    out.newlineOpt();
    addSubstitition(x);
    return false;
  }

  @Override
  public boolean visit(CssUrl x, Context ctx) {
    // These are not valid CSS
    out.printOpt("/* CssUrl */");
    out.newlineOpt();
    return false;
  }

  private <T extends CssNode & CssSubstitution> void addSubstitition(T node) {
    if (substituteDots) {
      out.printOpt(".....");
      out.newlineOpt();
    } else {
      int position = out.toString().length();
      if (substitutionPositions.containsKey(position)) {
        substitutionPositions.get(position).add(node);
      } else {
        List<CssSubstitution> nodes = new ArrayList<CssSubstitution>();
        nodes.add(node);
        substitutionPositions.put(position, nodes);
      }
    }
  }

  private void closeBrace() {
    out.indentOut();
    out.print('}');
    out.newlineOpt();
  }

  private void colon() {
    spaceOpt();
    out.print(':');
    spaceOpt();
  }

  private void comma() {
    out.print(',');
    spaceOpt();
  }

  private void important() {
    out.print(" !important");
  }

  private void openBrace() {
    spaceOpt();
    out.print('{');
    out.newlineOpt();
    out.indentIn();
  }

  private void semi() {
    out.print(';');
    out.newlineOpt();
  }

  private void spaceOpt() {
    out.printOpt(' ');
  }
}
