/*
 * Copyright 2011 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.dom.builder.shared;

import com.google.gwt.dom.client.AnchorElement;
import com.google.gwt.dom.client.AreaElement;
import com.google.gwt.dom.client.AudioElement;
import com.google.gwt.dom.client.BRElement;
import com.google.gwt.dom.client.BaseElement;
import com.google.gwt.dom.client.BodyElement;
import com.google.gwt.dom.client.ButtonElement;
import com.google.gwt.dom.client.CanvasElement;
import com.google.gwt.dom.client.DListElement;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.FieldSetElement;
import com.google.gwt.dom.client.FormElement;
import com.google.gwt.dom.client.FrameElement;
import com.google.gwt.dom.client.FrameSetElement;
import com.google.gwt.dom.client.HRElement;
import com.google.gwt.dom.client.HeadElement;
import com.google.gwt.dom.client.IFrameElement;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.dom.client.LIElement;
import com.google.gwt.dom.client.LabelElement;
import com.google.gwt.dom.client.LegendElement;
import com.google.gwt.dom.client.LinkElement;
import com.google.gwt.dom.client.MapElement;
import com.google.gwt.dom.client.MetaElement;
import com.google.gwt.dom.client.OListElement;
import com.google.gwt.dom.client.OptGroupElement;
import com.google.gwt.dom.client.OptionElement;
import com.google.gwt.dom.client.ParagraphElement;
import com.google.gwt.dom.client.ParamElement;
import com.google.gwt.dom.client.PreElement;
import com.google.gwt.dom.client.QuoteElement;
import com.google.gwt.dom.client.ScriptElement;
import com.google.gwt.dom.client.SelectElement;
import com.google.gwt.dom.client.SourceElement;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.dom.client.StyleElement;
import com.google.gwt.dom.client.TableCaptionElement;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableColElement;
import com.google.gwt.dom.client.TableElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.dom.client.TextAreaElement;
import com.google.gwt.dom.client.TitleElement;
import com.google.gwt.dom.client.UListElement;
import com.google.gwt.dom.client.VideoElement;
import com.google.gwt.safecss.shared.SafeStyles;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;

/**
 * Implementation of methods in {@link ElementBuilderBase} used to render HTML
 * as a string, using innerHtml to generate an element.
 */
class HtmlBuilderImpl extends ElementBuilderImpl {

  /*
   * Common element builders, and those most likely to appear in a loop, are
   * created on initialization to avoid null checks. Less common element
   * builders are created lazily to avoid unnecessary object creation.
   */
  private HtmlAnchorBuilder anchorBuilder;
  private HtmlAreaBuilder areaBuilder;
  private HtmlAudioBuilder audioBuilder;
  private HtmlBaseBuilder baseBuilder;
  private HtmlBodyBuilder bodyBuilder;
  private HtmlBRBuilder brBuilder;
  private HtmlButtonBuilder buttonBuilder;
  private HtmlCanvasBuilder canvasBuilder;
  private final HtmlDivBuilder divBuilder = new HtmlDivBuilder(this);
  private HtmlDListBuilder dListBuilder;
  private final HtmlElementBuilder elementBuilder = new HtmlElementBuilder(this);
  private HtmlFieldSetBuilder fieldSetBuilder;
  private HtmlFormBuilder formBuilder;
  private HtmlFrameBuilder frameBuilder;
  private HtmlFrameSetBuilder frameSetBuilder;
  private HtmlHeadBuilder headBuilder;
  private HtmlHeadingBuilder headingBuilder;
  private HtmlHRBuilder hrBuilder;
  private HtmlIFrameBuilder iFrameBuilder;
  private HtmlImageBuilder imageBuilder;
  private final HtmlInputBuilder inputBuilder = new HtmlInputBuilder(this);
  private HtmlLabelBuilder labelBuilder;
  private HtmlLegendBuilder legendBuilder;
  private final HtmlLIBuilder liBuilder = new HtmlLIBuilder(this);
  private HtmlLinkBuilder linkBuilder;
  private HtmlMapBuilder mapBuilder;
  private HtmlMetaBuilder metaBuilder;
  private HtmlOListBuilder oListBuilder;
  private final HtmlOptionBuilder optionBuilder = new HtmlOptionBuilder(this);
  private HtmlOptGroupBuilder optGroupBuilder;
  private HtmlParagraphBuilder paragraphBuilder;
  private HtmlParamBuilder paramBuilder;
  private HtmlPreBuilder preBuilder;
  private HtmlQuoteBuilder quoteBuilder;
  private HtmlScriptBuilder scriptBuilder;
  private HtmlSelectBuilder selectBuilder;
  private HtmlSourceBuilder sourceBuilder;
  private final HtmlSpanBuilder spanBuilder = new HtmlSpanBuilder(this);
  private HtmlStyleBuilder styleBuilder;
  private final StylesBuilder stylesBuilder = new HtmlStylesBuilder(this);
  private HtmlTableBuilder tableBuilder;
  private final HtmlTableCellBuilder tableCellBuilder = new HtmlTableCellBuilder(this);
  private HtmlTableCaptionBuilder tableCaptionBuilder;
  private HtmlTableColBuilder tableColBuilder;
  private HtmlTableColBuilder tableColGroupBuilder;
  private final HtmlTableRowBuilder tableRowBuilder = new HtmlTableRowBuilder(this);
  private HtmlTableSectionBuilder tableSectionBuilder;
  private HtmlTextAreaBuilder textAreaBuilder;
  private HtmlTitleBuilder titleBuilder;
  private HtmlUListBuilder uListBuilder;
  private HtmlVideoBuilder videoBuilder;

  /**
   * Used to builder the HTML string. We cannot use
   * {@link com.google.gwt.safehtml.shared.SafeHtmlBuilder} because it does some
   * rudimentary checks that the HTML tags are complete. Instead, we escape
   * values before appending them.
   */
  private final StringBuilder sb = new StringBuilder();

  /**
   * Return the HTML as a {@link SafeHtml} string.
   */
  public SafeHtml asSafeHtml() {
    // End all open tags.
    endAllTags();

    /*
     * sb is trusted because we only append trusted strings or escaped strings
     * to it.
     */
    return SafeHtmlUtils.fromTrustedString(sb.toString());
  }

  public void attribute(String name, int value) {
    trustedAttribute(escape(name), value);
  }

  public void attribute(String name, String value) {
    trustedAttribute(escape(name), value);
  }

  public HtmlAnchorBuilder startAnchor() {
    if (anchorBuilder == null) {
      anchorBuilder = new HtmlAnchorBuilder(this);
    }
    trustedStart(AnchorElement.TAG, anchorBuilder);
    return anchorBuilder;
  }

  public HtmlAreaBuilder startArea() {
    if (areaBuilder == null) {
      areaBuilder = new HtmlAreaBuilder(this);
    }
    trustedStart(AreaElement.TAG, areaBuilder);
    return areaBuilder;
  }

  public HtmlAudioBuilder startAudio() {
    if (audioBuilder == null) {
      audioBuilder = new HtmlAudioBuilder(this);
    }
    trustedStart(AudioElement.TAG, audioBuilder);
    return audioBuilder;
  }

  public HtmlBaseBuilder startBase() {
    if (baseBuilder == null) {
      baseBuilder = new HtmlBaseBuilder(this);
    }
    trustedStart(BaseElement.TAG, baseBuilder);
    return baseBuilder;
  }

  public HtmlQuoteBuilder startBlockQuote() {
    return startQuote(QuoteElement.TAG_BLOCKQUOTE);
  }

  public HtmlBodyBuilder startBody() {
    if (bodyBuilder == null) {
      bodyBuilder = new HtmlBodyBuilder(this);
    }
    trustedStart(BodyElement.TAG, bodyBuilder);
    return bodyBuilder;
  }

  public HtmlBRBuilder startBR() {
    if (brBuilder == null) {
      brBuilder = new HtmlBRBuilder(this);
    }
    trustedStart(BRElement.TAG, brBuilder);
    return brBuilder;
  }

  public HtmlInputBuilder startButtonInput() {
    return startInput(ButtonElement.TAG);
  }

  public HtmlCanvasBuilder startCanvas() {
    if (canvasBuilder == null) {
      canvasBuilder = new HtmlCanvasBuilder(this);
    }
    trustedStart(CanvasElement.TAG, canvasBuilder);
    return canvasBuilder;
  }

  public HtmlInputBuilder startCheckboxInput() {
    return startInput("checkbox");
  }

  public HtmlTableColBuilder startCol() {
    if (tableColBuilder == null) {
      tableColBuilder = new HtmlTableColBuilder(this, false);
    }
    trustedStart(TableColElement.TAG_COL, tableColBuilder);
    return tableColBuilder;
  }

  public HtmlTableColBuilder startColGroup() {
    if (tableColGroupBuilder == null) {
      tableColGroupBuilder = new HtmlTableColBuilder(this, true);
    }
    trustedStart(TableColElement.TAG_COLGROUP, tableColGroupBuilder);
    return tableColGroupBuilder;
  }

  public HtmlDivBuilder startDiv() {
    trustedStart(DivElement.TAG, divBuilder);
    return divBuilder;
  }

  public HtmlDListBuilder startDList() {
    if (dListBuilder == null) {
      dListBuilder = new HtmlDListBuilder(this);
    }
    trustedStart(DListElement.TAG, dListBuilder);
    return dListBuilder;
  }

  public HtmlFieldSetBuilder startFieldSet() {
    if (fieldSetBuilder == null) {
      fieldSetBuilder = new HtmlFieldSetBuilder(this);
    }
    trustedStart(FieldSetElement.TAG, fieldSetBuilder);
    return fieldSetBuilder;
  }

  public HtmlInputBuilder startFileInput() {
    return startInput("file");
  }

  public HtmlFormBuilder startForm() {
    if (formBuilder == null) {
      formBuilder = new HtmlFormBuilder(this);
    }
    trustedStart(FormElement.TAG, formBuilder);
    return formBuilder;
  }

  public HtmlFrameBuilder startFrame() {
    if (frameBuilder == null) {
      frameBuilder = new HtmlFrameBuilder(this);
    }
    trustedStart(FrameElement.TAG, frameBuilder);
    return frameBuilder;
  }

  public HtmlFrameSetBuilder startFrameSet() {
    if (frameSetBuilder == null) {
      frameSetBuilder = new HtmlFrameSetBuilder(this);
    }
    trustedStart(FrameSetElement.TAG, frameSetBuilder);
    return frameSetBuilder;
  }

  public HtmlHeadingBuilder startH1() {
    return startHeading(1);
  }

  public HtmlHeadingBuilder startH2() {
    return startHeading(2);
  }

  public HtmlHeadingBuilder startH3() {
    return startHeading(3);
  }

  public HtmlHeadingBuilder startH4() {
    return startHeading(4);
  }

  public HtmlHeadingBuilder startH5() {
    return startHeading(5);
  }

  public HtmlHeadingBuilder startH6() {
    return startHeading(6);
  }

  public HtmlHeadBuilder startHead() {
    if (headBuilder == null) {
      headBuilder = new HtmlHeadBuilder(this);
    }
    trustedStart(HeadElement.TAG, headBuilder);
    return headBuilder;
  }

  public HtmlInputBuilder startHiddenInput() {
    return startInput("hidden");
  }

  public HtmlHRBuilder startHR() {
    if (hrBuilder == null) {
      hrBuilder = new HtmlHRBuilder(this);
    }
    trustedStart(HRElement.TAG, hrBuilder);
    return hrBuilder;
  }

  public HtmlIFrameBuilder startIFrame() {
    if (iFrameBuilder == null) {
      iFrameBuilder = new HtmlIFrameBuilder(this);
    }
    trustedStart(IFrameElement.TAG, iFrameBuilder);
    return iFrameBuilder;
  }

  public HtmlImageBuilder startImage() {
    if (imageBuilder == null) {
      imageBuilder = new HtmlImageBuilder(this);
    }
    trustedStart(ImageElement.TAG, imageBuilder);
    return imageBuilder;
  }

  public HtmlInputBuilder startImageInput() {
    return startInput("image");
  }

  public HtmlLabelBuilder startLabel() {
    if (labelBuilder == null) {
      labelBuilder = new HtmlLabelBuilder(this);
    }
    trustedStart(LabelElement.TAG, labelBuilder);
    return labelBuilder;
  }

  public HtmlLegendBuilder startLegend() {
    if (legendBuilder == null) {
      legendBuilder = new HtmlLegendBuilder(this);
    }
    trustedStart(LegendElement.TAG, legendBuilder);
    return legendBuilder;
  }

  public HtmlLIBuilder startLI() {
    trustedStart(LIElement.TAG, liBuilder);
    return liBuilder;
  }

  public HtmlLinkBuilder startLink() {
    if (linkBuilder == null) {
      linkBuilder = new HtmlLinkBuilder(this);
    }
    trustedStart(LinkElement.TAG, linkBuilder);
    return linkBuilder;
  }

  public HtmlMapBuilder startMap() {
    if (mapBuilder == null) {
      mapBuilder = new HtmlMapBuilder(this);
    }
    trustedStart(MapElement.TAG, mapBuilder);
    return mapBuilder;
  }

  public HtmlMetaBuilder startMeta() {
    if (metaBuilder == null) {
      metaBuilder = new HtmlMetaBuilder(this);
    }
    trustedStart(MetaElement.TAG, metaBuilder);
    return metaBuilder;
  }

  public HtmlOListBuilder startOList() {
    if (oListBuilder == null) {
      oListBuilder = new HtmlOListBuilder(this);
    }
    trustedStart(OListElement.TAG, oListBuilder);
    return oListBuilder;
  }

  public HtmlOptGroupBuilder startOptGroup() {
    if (optGroupBuilder == null) {
      optGroupBuilder = new HtmlOptGroupBuilder(this);
    }
    trustedStart(OptGroupElement.TAG, optGroupBuilder);
    return optGroupBuilder;
  }

  public HtmlOptionBuilder startOption() {
    trustedStart(OptionElement.TAG, optionBuilder);
    return optionBuilder;
  }

  public HtmlParagraphBuilder startParagraph() {
    if (paragraphBuilder == null) {
      paragraphBuilder = new HtmlParagraphBuilder(this);
    }
    trustedStart(ParagraphElement.TAG, paragraphBuilder);
    return paragraphBuilder;
  }

  public HtmlParamBuilder startParam() {
    if (paramBuilder == null) {
      paramBuilder = new HtmlParamBuilder(this);
    }
    trustedStart(ParamElement.TAG, paramBuilder);
    return paramBuilder;
  }

  public HtmlInputBuilder startPasswordInput() {
    return startInput("password");
  }

  public HtmlPreBuilder startPre() {
    if (preBuilder == null) {
      preBuilder = new HtmlPreBuilder(this);
    }
    trustedStart(PreElement.TAG, preBuilder);
    return preBuilder;
  }

  public HtmlButtonBuilder startPushButton() {
    return startButton("button");
  }

  public HtmlQuoteBuilder startQuote() {
    return startQuote(QuoteElement.TAG_Q);
  }

  public HtmlInputBuilder startRadioInput(String name) {
    HtmlInputBuilder builder = startInput("radio");
    attribute("name", name);
    return builder;
  }

  public HtmlButtonBuilder startResetButton() {
    return startButton("reset");
  }

  public HtmlInputBuilder startResetInput() {
    return startInput("reset");
  }

  public HtmlScriptBuilder startScript() {
    if (scriptBuilder == null) {
      scriptBuilder = new HtmlScriptBuilder(this);
    }
    trustedStart(ScriptElement.TAG, scriptBuilder);
    return scriptBuilder;
  }

  public HtmlSelectBuilder startSelect() {
    if (selectBuilder == null) {
      selectBuilder = new HtmlSelectBuilder(this);
    }
    trustedStart(SelectElement.TAG, selectBuilder);
    return selectBuilder;
  }

  public HtmlSourceBuilder startSource() {
    if (sourceBuilder == null) {
      sourceBuilder = new HtmlSourceBuilder(this);
    }
    trustedStart(SourceElement.TAG, sourceBuilder);
    return sourceBuilder;
  }

  public HtmlSpanBuilder startSpan() {
    trustedStart(SpanElement.TAG, spanBuilder);
    return spanBuilder;
  }

  public HtmlStyleBuilder startStyle() {
    if (styleBuilder == null) {
      styleBuilder = new HtmlStyleBuilder(this);
    }
    trustedStart(StyleElement.TAG, styleBuilder);
    return styleBuilder;
  }

  public HtmlButtonBuilder startSubmitButton() {
    return startButton("submit");
  }

  public HtmlInputBuilder startSubmitInput() {
    return startInput("submit");
  }

  public HtmlTableBuilder startTable() {
    if (tableBuilder == null) {
      tableBuilder = new HtmlTableBuilder(this);
    }
    trustedStart(TableElement.TAG, tableBuilder);
    return tableBuilder;
  }

  public HtmlTableCaptionBuilder startTableCaption() {
    if (tableCaptionBuilder == null) {
      tableCaptionBuilder = new HtmlTableCaptionBuilder(this);
    }
    trustedStart(TableCaptionElement.TAG, tableCaptionBuilder);
    return tableCaptionBuilder;
  }

  public HtmlTableSectionBuilder startTBody() {
    return startTableSection(TableSectionElement.TAG_TBODY);
  }

  public HtmlTableCellBuilder startTD() {
    trustedStart(TableCellElement.TAG_TD, tableCellBuilder);
    return tableCellBuilder;
  }

  public HtmlTextAreaBuilder startTextArea() {
    if (textAreaBuilder == null) {
      textAreaBuilder = new HtmlTextAreaBuilder(this);
    }
    trustedStart(TextAreaElement.TAG, textAreaBuilder);
    return textAreaBuilder;
  }

  public HtmlInputBuilder startTextInput() {
    return startInput("text");
  }

  public HtmlTableSectionBuilder startTFoot() {
    return startTableSection(TableSectionElement.TAG_TFOOT);
  }

  public HtmlTableCellBuilder startTH() {
    trustedStart(TableCellElement.TAG_TH, tableCellBuilder);
    return tableCellBuilder;
  }

  public HtmlTableSectionBuilder startTHead() {
    return startTableSection(TableSectionElement.TAG_THEAD);
  }

  public HtmlTitleBuilder startTitle() {
    if (titleBuilder == null) {
      titleBuilder = new HtmlTitleBuilder(this);
    }
    trustedStart(TitleElement.TAG, titleBuilder);
    return titleBuilder;
  }

  public HtmlTableRowBuilder startTR() {
    trustedStart(TableRowElement.TAG, tableRowBuilder);
    return tableRowBuilder;
  }

  public HtmlUListBuilder startUList() {
    if (uListBuilder == null) {
      uListBuilder = new HtmlUListBuilder(this);
    }
    trustedStart(UListElement.TAG, uListBuilder);
    return uListBuilder;
  }

  public HtmlVideoBuilder startVideo() {
    if (videoBuilder == null) {
      videoBuilder = new HtmlVideoBuilder(this);
    }
    trustedStart(VideoElement.TAG, videoBuilder);
    return videoBuilder;
  }

  @Override
  public StylesBuilder style() {
    return stylesBuilder;
  }

  public StylesBuilder styleProperty(SafeStyles style) {
    assertCanAddStylePropertyImpl();
    sb.append(style.asString());
    return style();
  }

  /**
   * Add a trusted attribute without escaping the name.
   */
  public void trustedAttribute(String name, int value) {
    assertCanAddAttributeImpl();
    sb.append(" ").append(name).append("=\"").append(value).append("\"");
  }

  /**
   * Add a trusted attribute without escaping the name. The value is still
   * escaped.
   */
  public void trustedAttribute(String name, String value) {
    assertCanAddAttributeImpl();
    sb.append(" ").append(name).append("=\"").append(escape(value)).append("\"");
  }

  public HtmlElementBuilder trustedStart(String tagName) {
    trustedStart(tagName, elementBuilder);
    return elementBuilder;
  }

  @Override
  protected void doCloseStartTagImpl() {
    sb.append(">");
  }

  @Override
  protected void doCloseStyleAttributeImpl() {
    sb.append("\"");
  }

  @Override
  protected void doEndStartTagImpl() {
    sb.append(" />");
  }

  @Override
  protected void doEndTagImpl(String tagName) {
    /*
     * Add an end tag.
     * 
     * Some browsers do not behave correctly if you self close (ex <select />)
     * certain tags, so we always add the end tag unless the element
     * specifically forbids an end tag (see doEndStartTagImpl()).
     * 
     * The tag name is safe because it comes from the stack, and tag names are
     * checked before they are added to the stack.
     */
    sb.append("</").append(tagName).append(">");
  }

  @Override
  protected Element doFinishImpl() {
    Element tmp = Document.get().createDivElement();
    tmp.setInnerSafeHtml(asSafeHtml());
    return tmp.getFirstChildElement();
  }

  @Override
  protected void doHtmlImpl(SafeHtml html) {
    sb.append(html.asString());
  }

  @Override
  protected void doOpenStyleImpl() {
    sb.append(" style=\"");
  }

  @Override
  protected void doTextImpl(String text) {
    sb.append(escape(text));
  }

  /**
   * Escape a string.
   * 
   * @param s the string to escape
   */
  private String escape(String s) {
    return SafeHtmlUtils.htmlEscape(s);
  }

  /**
   * Start a button with the specified type.
   */
  private HtmlButtonBuilder startButton(String type) {
    if (buttonBuilder == null) {
      buttonBuilder = new HtmlButtonBuilder(this);
    }
    trustedStart("button", buttonBuilder);
    buttonBuilder.attribute("type", type);
    return buttonBuilder;
  }

  /**
   * Start one of the many heading elements.
   */
  private HtmlHeadingBuilder startHeading(int level) {
    if (headingBuilder == null) {
      headingBuilder = new HtmlHeadingBuilder(this);
    }
    trustedStart("h" + level, headingBuilder);
    return headingBuilder;
  }

  /**
   * Start an input with the specified type.
   */
  private HtmlInputBuilder startInput(String type) {
    trustedStart("input", inputBuilder);
    attribute("type", type);
    return inputBuilder;
  }

  /**
   * Start a quote or blockquote.
   */
  private HtmlQuoteBuilder startQuote(String tagName) {
    if (quoteBuilder == null) {
      quoteBuilder = new HtmlQuoteBuilder(this);
    }
    trustedStart(tagName, quoteBuilder);
    return quoteBuilder;
  }

  /**
   * Start a table section of the specified tag name.
   */
  private HtmlTableSectionBuilder startTableSection(String tagName) {
    if (tableSectionBuilder == null) {
      tableSectionBuilder = new HtmlTableSectionBuilder(this);
    }
    trustedStart(tagName, tableSectionBuilder);
    return tableSectionBuilder;
  }

  /**
   * Start a tag using the specified builder. The tagName is not checked or
   * escaped.
   */
  private void trustedStart(String tagName, ElementBuilderBase<?> builder) {
    onStart(tagName, builder);
    sb.append("<").append(tagName);
  }
}
