/*
 * Copyright 2000-2015 JetBrains s.r.o.
 *
 * 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.intellij.openapi.fileTypes.impl;

import com.intellij.ide.highlighter.FileTypeRegistrator;
import com.intellij.ide.highlighter.custom.SyntaxTable;
import com.intellij.ide.highlighter.custom.impl.CustomFileTypeEditor;
import com.intellij.lang.Commenter;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileTypes.*;
import com.intellij.openapi.fileTypes.ex.ExternalizableFileType;
import com.intellij.openapi.options.ExternalizableScheme;
import com.intellij.openapi.options.SettingsEditor;
import com.intellij.openapi.util.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.SmartList;
import com.intellij.util.text.StringTokenizer;
import org.jdom.Element;
import org.jdom.output.XMLOutputter;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;

public class AbstractFileType extends UserFileType<AbstractFileType> implements ExternalizableFileType, ExternalizableScheme,
                                                                                CustomSyntaxTableFileType {
  private static final String SEMICOLON = ";";
  protected SyntaxTable mySyntaxTable;
  private SyntaxTable myDefaultSyntaxTable;
  protected Commenter myCommenter = null;
  @NonNls public static final String ELEMENT_HIGHLIGHTING = "highlighting";
  @NonNls private static final String ELEMENT_OPTIONS = "options";
  @NonNls private static final String ELEMENT_OPTION = "option";
  @NonNls private static final String ATTRIBUTE_VALUE = "value";
  @NonNls private static final String VALUE_LINE_COMMENT = "LINE_COMMENT";
  @NonNls private static final String VALUE_COMMENT_START = "COMMENT_START";
  @NonNls private static final String VALUE_COMMENT_END = "COMMENT_END";
  @NonNls private static final String VALUE_HEX_PREFIX = "HEX_PREFIX";
  @NonNls private static final String VALUE_NUM_POSTFIXES = "NUM_POSTFIXES";
  @NonNls private static final String VALUE_HAS_BRACES = "HAS_BRACES";
  @NonNls private static final String VALUE_HAS_BRACKETS = "HAS_BRACKETS";
  @NonNls private static final String VALUE_HAS_PARENS = "HAS_PARENS";
  @NonNls private static final String VALUE_HAS_STRING_ESCAPES = "HAS_STRING_ESCAPES";
  @NonNls private static final String VALUE_LINE_COMMENT_AT_START = "LINE_COMMENT_AT_START";
  @NonNls private static final String ELEMENT_KEYWORDS = "keywords";
  @NonNls private static final String ATTRIBUTE_IGNORE_CASE = "ignore_case";
  @NonNls private static final String ELEMENT_KEYWORD = "keyword";
  @NonNls private static final String ELEMENT_KEYWORDS2 = "keywords2";
  @NonNls private static final String ELEMENT_KEYWORDS3 = "keywords3";
  @NonNls private static final String ELEMENT_KEYWORDS4 = "keywords4";
  @NonNls private static final String ATTRIBUTE_NAME = "name";
  @NonNls public static final String ELEMENT_EXTENSION_MAP = "extensionMap";

  public AbstractFileType(SyntaxTable syntaxTable) {
    mySyntaxTable = syntaxTable;
  }

  public void initSupport() {
    for (FileTypeRegistrator registrator : Extensions.getRootArea().getExtensionPoint(FileTypeRegistrator.EP_NAME).getExtensions()) {
      registrator.initFileType(this);
    }
  }

  @Override
  public SyntaxTable getSyntaxTable() {
    return mySyntaxTable;
  }

  public Commenter getCommenter() {
    return myCommenter;
  }

  public void setSyntaxTable(SyntaxTable syntaxTable) {
    mySyntaxTable = syntaxTable;
  }

  @Override
  public AbstractFileType clone() {
    return (AbstractFileType)super.clone();
  }

  @Override
  public void copyFrom(@NotNull UserFileType newType) {
    super.copyFrom(newType);

    if (newType instanceof AbstractFileType) {
      mySyntaxTable = ((CustomSyntaxTableFileType)newType).getSyntaxTable();
    }
  }

  @Override
  public boolean isBinary() {
    return false;
  }

  @Override
  public void readExternal(@NotNull Element typeElement) throws InvalidDataException {
    Element element = typeElement.getChild(ELEMENT_HIGHLIGHTING);
    if (element != null) {
      setSyntaxTable(readSyntaxTable(element));
    }
  }

  @NotNull
  public static SyntaxTable readSyntaxTable(@NotNull Element root) {
    SyntaxTable table = new SyntaxTable();

    for (Element element : root.getChildren()) {
      if (ELEMENT_OPTIONS.equals(element.getName())) {
        for (final Object o1 : element.getChildren(ELEMENT_OPTION)) {
          Element e = (Element)o1;
          String name = e.getAttributeValue(ATTRIBUTE_NAME);
          String value = e.getAttributeValue(ATTRIBUTE_VALUE);
          if (VALUE_LINE_COMMENT.equals(name)) {
            table.setLineComment(value);
          }
          else if (VALUE_COMMENT_START.equals(name)) {
            table.setStartComment(value);
          }
          else if (VALUE_COMMENT_END.equals(name)) {
            table.setEndComment(value);
          }
          else if (VALUE_HEX_PREFIX.equals(name)) {
            table.setHexPrefix(value);
          }
          else if (VALUE_NUM_POSTFIXES.equals(name)) {
            table.setNumPostfixChars(value);
          }
          else if (VALUE_LINE_COMMENT_AT_START.equals(name)) {
            table.lineCommentOnlyAtStart = Boolean.valueOf(value).booleanValue();
          }
          else if (VALUE_HAS_BRACES.equals(name)) {
            table.setHasBraces(Boolean.valueOf(value).booleanValue());
          }
          else if (VALUE_HAS_BRACKETS.equals(name)) {
            table.setHasBrackets(Boolean.valueOf(value).booleanValue());
          }
          else if (VALUE_HAS_PARENS.equals(name)) {
            table.setHasParens(Boolean.valueOf(value).booleanValue());
          }
          else if (VALUE_HAS_STRING_ESCAPES.equals(name)) {
            table.setHasStringEscapes(Boolean.valueOf(value).booleanValue());
          }
        }
      }
      else if (ELEMENT_KEYWORDS.equals(element.getName())) {
        boolean ignoreCase = Boolean.valueOf(element.getAttributeValue(ATTRIBUTE_IGNORE_CASE)).booleanValue();
        table.setIgnoreCase(ignoreCase);
        loadKeywords(element, table.getKeywords1());
      }
      else if (ELEMENT_KEYWORDS2.equals(element.getName())) {
        loadKeywords(element, table.getKeywords2());
      }
      else if (ELEMENT_KEYWORDS3.equals(element.getName())) {
        loadKeywords(element, table.getKeywords3());
      }
      else if (ELEMENT_KEYWORDS4.equals(element.getName())) {
        loadKeywords(element, table.getKeywords4());
      }
    }

    boolean DUMP_TABLE = false;
    if (DUMP_TABLE) {
      Element element = new Element("temp");
      writeTable(element, table);
      XMLOutputter outputter = JDOMUtil.createOutputter("\n");
      try {
        outputter.output((Element)element.getContent().get(0), System.out);
      }
      catch (IOException ignored) {
      }
    }
    return table;
  }

  private static void loadKeywords(Element element, Set<String> keywords) {
    String value = element.getAttributeValue(ELEMENT_KEYWORDS);
    if (value != null) {
      StringTokenizer tokenizer = new StringTokenizer(value, SEMICOLON);
      while(tokenizer.hasMoreElements()) {
        String keyword = tokenizer.nextToken().trim();
        if (keyword.length() != 0) keywords.add(keyword);
      }
    }
    for (final Object o1 : element.getChildren(ELEMENT_KEYWORD)) {
      keywords.add(((Element)o1).getAttributeValue(ATTRIBUTE_NAME));
    }
  }

  @Override
  public void writeExternal(@NotNull Element element) {
    writeTable(element, getSyntaxTable());
  }

  private static void writeTable(@NotNull Element element, @NotNull SyntaxTable table) {
    Element highlightingElement = new Element(ELEMENT_HIGHLIGHTING);

    Element optionsElement = new Element(ELEMENT_OPTIONS);

    Element lineComment = new Element(ELEMENT_OPTION);
    lineComment.setAttribute(ATTRIBUTE_NAME, VALUE_LINE_COMMENT);
    lineComment.setAttribute(ATTRIBUTE_VALUE, table.getLineComment());
    optionsElement.addContent(lineComment);

    String commentStart = table.getStartComment();
    if (commentStart != null) {
      Element commentStartElement = new Element(ELEMENT_OPTION);
      commentStartElement.setAttribute(ATTRIBUTE_NAME, VALUE_COMMENT_START);
      commentStartElement.setAttribute(ATTRIBUTE_VALUE, commentStart);
      optionsElement.addContent(commentStartElement);
    }

    String endComment = table.getEndComment();

    if (endComment != null) {
      Element commentEndElement = new Element(ELEMENT_OPTION);
      commentEndElement.setAttribute(ATTRIBUTE_NAME, VALUE_COMMENT_END);
      commentEndElement.setAttribute(ATTRIBUTE_VALUE, endComment);
      optionsElement.addContent(commentEndElement);
    }

    String prefix = table.getHexPrefix();

    if (prefix != null) {
      Element hexPrefix = new Element(ELEMENT_OPTION);
      hexPrefix.setAttribute(ATTRIBUTE_NAME, VALUE_HEX_PREFIX);
      hexPrefix.setAttribute(ATTRIBUTE_VALUE, prefix);
      optionsElement.addContent(hexPrefix);
    }

    String chars = table.getNumPostfixChars();

    if (chars != null) {
      Element numPostfixes = new Element(ELEMENT_OPTION);
      numPostfixes.setAttribute(ATTRIBUTE_NAME, VALUE_NUM_POSTFIXES);
      numPostfixes.setAttribute(ATTRIBUTE_VALUE, chars);
      optionsElement.addContent(numPostfixes);
    }

    addElementOption(optionsElement, VALUE_HAS_BRACES, table.isHasBraces());
    addElementOption(optionsElement, VALUE_HAS_BRACKETS, table.isHasBrackets());
    addElementOption(optionsElement, VALUE_HAS_PARENS, table.isHasParens());
    addElementOption(optionsElement, VALUE_HAS_STRING_ESCAPES, table.isHasStringEscapes());
    addElementOption(optionsElement, VALUE_LINE_COMMENT_AT_START, table.lineCommentOnlyAtStart);

    highlightingElement.addContent(optionsElement);

    writeKeywords(table.getKeywords1(), ELEMENT_KEYWORDS, highlightingElement).setAttribute(ATTRIBUTE_IGNORE_CASE, String.valueOf(table.isIgnoreCase()));
    writeKeywords(table.getKeywords2(), ELEMENT_KEYWORDS2, highlightingElement);
    writeKeywords(table.getKeywords3(), ELEMENT_KEYWORDS3, highlightingElement);
    writeKeywords(table.getKeywords4(), ELEMENT_KEYWORDS4, highlightingElement);

    element.addContent(highlightingElement);
  }

  private static void addElementOption(final Element optionsElement, final String valueHasParens, final boolean hasParens) {
    if (!hasParens) {
      return;
    }

    Element supportParens = new Element(ELEMENT_OPTION);
    supportParens.setAttribute(ATTRIBUTE_NAME, valueHasParens);
    supportParens.setAttribute(ATTRIBUTE_VALUE, String.valueOf(true));
    optionsElement.addContent(supportParens);
  }

  private static Element writeKeywords(Set<String> keywords, String tagName, Element highlightingElement) {
    if (keywords.size() == 0 && !ELEMENT_KEYWORDS.equals(tagName)) return null;
    Element keywordsElement = new Element(tagName);
    String[] strings = ArrayUtil.toStringArray(keywords);
    Arrays.sort(strings);
    StringBuilder keywordsAttribute = new StringBuilder();

    for (final String keyword : strings) {
      if (!keyword.contains(SEMICOLON)) {
        if (keywordsAttribute.length() != 0) keywordsAttribute.append(SEMICOLON);
        keywordsAttribute.append(keyword);
      } else {
        Element e = new Element(ELEMENT_KEYWORD);
        e.setAttribute(ATTRIBUTE_NAME, keyword);
        keywordsElement.addContent(e);
      }
    }
    if (keywordsAttribute.length() != 0) {
      keywordsElement.setAttribute(ELEMENT_KEYWORDS, keywordsAttribute.toString());
    }
    highlightingElement.addContent(keywordsElement);
    return keywordsElement;
  }

  @Override
  public void markDefaultSettings() {
    myDefaultSyntaxTable = mySyntaxTable;
  }

  @Override
  public boolean isModified() {
    return !Comparing.equal(myDefaultSyntaxTable, getSyntaxTable());
  }

  @NonNls private static final String ELEMENT_MAPPING = "mapping";
  @NonNls private static final String ATTRIBUTE_EXT = "ext";
  @NonNls private static final String ATTRIBUTE_PATTERN = "pattern";
  /** Applied for removed mappings approved by user */
  @NonNls private static final String ATTRIBUTE_APPROVED = "approved";

  @NonNls private static final String ELEMENT_REMOVED_MAPPING = "removed_mapping";
  @NonNls private static final String ATTRIBUTE_TYPE = "type";

  @NotNull
  public static List<Pair<FileNameMatcher, String>> readAssociations(@NotNull Element element) {
    List<Element> children = element.getChildren(ELEMENT_MAPPING);
    if (children.isEmpty()) {
      return Collections.emptyList();
    }

    List<Pair<FileNameMatcher, String>> result = new SmartList<>();
    for (Element mapping : children) {
      String ext = mapping.getAttributeValue(ATTRIBUTE_EXT);
      String pattern = mapping.getAttributeValue(ATTRIBUTE_PATTERN);

      FileNameMatcher matcher = ext != null ? new ExtensionFileNameMatcher(ext) : FileTypeManager.parseFromString(pattern);
      result.add(Pair.create(matcher, mapping.getAttributeValue(ATTRIBUTE_TYPE)));
    }
    return result;
  }

  @NotNull
  public static List<Trinity<FileNameMatcher, String, Boolean>> readRemovedAssociations(@NotNull Element element) {
    List<Trinity<FileNameMatcher, String, Boolean>> result = new SmartList<>();
    List<Element> children = element.getChildren(ELEMENT_REMOVED_MAPPING);
    if (children.isEmpty()) {
      return Collections.emptyList();
    }

    for (Element mapping : children) {
      String ext = mapping.getAttributeValue(ATTRIBUTE_EXT);
      FileNameMatcher matcher = ext == null ? FileTypeManager.parseFromString(mapping.getAttributeValue(ATTRIBUTE_PATTERN)) : new ExtensionFileNameMatcher(ext);
      result.add(Trinity.create(matcher, mapping.getAttributeValue(ATTRIBUTE_TYPE), Boolean.parseBoolean(mapping.getAttributeValue(ATTRIBUTE_APPROVED))));
    }
    return result;
  }

  @Nullable
  public static Element writeMapping(String typeName, @NotNull FileNameMatcher matcher, boolean specifyTypeName) {
    Element mapping = new Element(ELEMENT_MAPPING);
    if (matcher instanceof ExtensionFileNameMatcher) {
      mapping.setAttribute(ATTRIBUTE_EXT, ((ExtensionFileNameMatcher)matcher).getExtension());
    }
    else if (matcher instanceof WildcardFileNameMatcher) {
      mapping.setAttribute(ATTRIBUTE_PATTERN, ((WildcardFileNameMatcher)matcher).getPattern());
    }
    else if (matcher instanceof ExactFileNameMatcher) {
      mapping.setAttribute(ATTRIBUTE_PATTERN, ((ExactFileNameMatcher)matcher).getFileName());
    }
    else {
      return null;
    }

    if (specifyTypeName) {
      mapping.setAttribute(ATTRIBUTE_TYPE, typeName);
    }

    return mapping;
  }

  static Element writeRemovedMapping(final FileType type, final FileNameMatcher matcher, final boolean specifyTypeName, boolean approved) {
    Element mapping = new Element(ELEMENT_REMOVED_MAPPING);
    if (matcher instanceof ExtensionFileNameMatcher) {
      mapping.setAttribute(ATTRIBUTE_EXT, ((ExtensionFileNameMatcher)matcher).getExtension());
      if (approved) {
        mapping.setAttribute(ATTRIBUTE_APPROVED, "true");
      }
    }
    else if (matcher instanceof WildcardFileNameMatcher) {
      mapping.setAttribute(ATTRIBUTE_PATTERN, ((WildcardFileNameMatcher)matcher).getPattern());
    }
    else if (matcher instanceof ExactFileNameMatcher) {
      mapping.setAttribute(ATTRIBUTE_PATTERN, ((ExactFileNameMatcher)matcher).getFileName());
    }
    else {
      return null;
    }
    if (specifyTypeName) {
      mapping.setAttribute(ATTRIBUTE_TYPE, type.getName());
    }

    return mapping;
  }

  @Override
  public SettingsEditor<AbstractFileType> getEditor() {
    return new CustomFileTypeEditor();
  }

  public void setCommenter(final Commenter commenter) {
    myCommenter = commenter;
  }
}
