package nokogiri.internals;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import nokogiri.NokogiriService;
import nokogiri.XmlAttr;
import nokogiri.XmlDocument;
import nokogiri.XmlSyntaxError;

import org.apache.xerces.xni.XMLAttributes;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBoolean;
import org.jruby.RubyHash;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;

import static nokogiri.internals.NokogiriHelpers.*;

/**
 * Abstract class of Node for XmlReader.
 *
 * @author Yoko Harada <yokolet@gmail.com>
 *
 */
public abstract class ReaderNode
{

  final Ruby ruby;
  public ReaderAttributeList attributeList;
  public Map<String, String> namespaces;
  public int depth, nodeType;
  public String lang, localName, xmlBase, prefix, name, uri, value, xmlVersion = "1.0";
  public int startOffset, endOffset;
  public boolean hasChildren = false;
  private Document document = null;

  protected
  ReaderNode(final Ruby runtime)
  {
    this.ruby = runtime;
  }

  public abstract String getString();

  public IRubyObject
  getAttributeByIndex(IRubyObject index)
  {
    if (index.isNil()) { return index; }

    long i = index.convertToInteger().getLongValue();
    if (i > Integer.MAX_VALUE) {
      throw ruby.newArgumentError("value too long to be an array index");
    }

    if (attributeList == null) { return ruby.getNil(); }
    if (i < 0 || attributeList.length <= i) { return ruby.getNil(); }
    return stringOrBlank(ruby, attributeList.values.get(((Long)i).intValue()));
  }

  public IRubyObject
  getAttributeByName(IRubyObject name)
  {
    if (attributeList == null) { return ruby.getNil(); }
    String value = attributeList.getByName(rubyStringToString(name));
    return stringOrNil(ruby, value);
  }

  public IRubyObject
  getAttributeByName(String name)
  {
    if (attributeList == null) { return ruby.getNil(); }
    String value = attributeList.getByName(name);
    return stringOrNil(ruby, value);
  }

  public IRubyObject
  getAttributeCount()
  {
    if (attributeList == null) { return ruby.newFixnum(0); }
    return ruby.newFixnum(attributeList.length);
  }

  public IRubyObject
  getAttributesNodes()
  {
    RubyArray<?> array = RubyArray.newArray(ruby);
    if (attributeList != null && attributeList.length > 0) {
      if (document == null) {
        document = XmlDocument.createNewDocument(ruby);
      }
      for (int i = 0; i < attributeList.length; i++) {
        if (!isNamespace(attributeList.names.get(i))) {
          Attr attr = document.createAttributeNS(attributeList.namespaces.get(i), attributeList.names.get(i));
          attr.setValue(attributeList.values.get(i));
          XmlAttr xmlAttr = new XmlAttr(ruby, attr);
          array.append(xmlAttr);
        }
      }
    }
    return array;
  }

  public IRubyObject
  getAttributes(ThreadContext context)
  {
    final Ruby runtime = context.runtime;
    RubyHash hash = RubyHash.newHash(runtime);
    if (attributeList == null) { return hash; }
    for (int i = 0; i < attributeList.length; i++) {
      if (isNamespace(attributeList.names.get(i))) { continue; }
      IRubyObject k = stringOrBlank(runtime, attributeList.names.get(i));
      IRubyObject v = stringOrBlank(runtime, attributeList.values.get(i));
      hash.fastASetCheckString(runtime, k, v); // hash.op_aset(context, k, v)
    }
    return hash;
  }

  public IRubyObject
  getDepth()
  {
    return ruby.newFixnum(depth);
  }

  public IRubyObject
  getLang()
  {
    return stringOrNil(ruby, lang);
  }

  public IRubyObject
  getLocalName()
  {
    return stringOrNil(ruby, localName);
  }

  public IRubyObject
  getName()
  {
    return stringOrNil(ruby, name);
  }

  public IRubyObject
  getNamespaces(ThreadContext context)
  {
    final Ruby runtime = context.runtime;
    RubyHash hash = RubyHash.newHash(runtime);
    if (namespaces == null) { return hash; }
    for (Map.Entry<String, String> entry : namespaces.entrySet()) {
      IRubyObject k = stringOrBlank(runtime, entry.getKey());
      IRubyObject v = stringOrBlank(runtime, entry.getValue());
      hash.fastASetCheckString(runtime, k, v); // hash.op_aset(context, k, v)
    }
    return hash;
  }

  public IRubyObject
  getXmlBase()
  {
    return stringOrNil(ruby, xmlBase);
  }

  public IRubyObject
  getPrefix()
  {
    return stringOrNil(ruby, prefix);
  }

  public IRubyObject
  getUri()
  {
    return stringOrNil(ruby, uri);
  }

  public IRubyObject
  getValue()
  {
    return stringOrNil(ruby, value);
  }

  public IRubyObject
  getXmlVersion()
  {
    return ruby.newString(xmlVersion);
  }

  public RubyBoolean
  hasAttributes()
  {
    if (attributeList == null || attributeList.length == 0) { return ruby.getFalse(); }
    return ruby.getTrue();
  }

  public abstract RubyBoolean hasValue();

  public RubyBoolean
  isDefault()
  {
    // TODO Implement.
    return ruby.getFalse();
  }

  public boolean
  isError() { return false; }

  protected void
  parsePrefix(String qName)
  {
    int index = qName.indexOf(':');
    if (index != -1) { prefix = qName.substring(0, index); }
  }

  public void
  setLang(String lang)
  {
    this.lang = lang;
  }

  public IRubyObject
  toSyntaxError() { return ruby.getNil(); }

  public IRubyObject
  getNodeType() { return ruby.newFixnum(nodeType); }

  public static enum ReaderNodeType {
    NODE(0),
    ELEMENT(1),
    ATTRIBUTE(2),
    TEXT(3),
    CDATA(4),
    ENTITY_REFERENCE(5),
    ENTITY(6),
    PROCESSING_INSTRUCTION(7),
    COMMENT(8),
    DOCUMENT(9),
    DOCUMENT_TYPE(10),
    DOCUMENTFRAGMENT(11),
    NOTATION(12),
    WHITESPACE(13),
    SIGNIFICANT_WHITESPACE(14),
    END_ELEMENT(15),
    END_ENTITY(16),
    XML_DECLARATION(17);

    private final int value;
    ReaderNodeType(int value)
    {
      this.value = value;
    }

    public int getValue()
    {
      return value;
    }
  }

  public static ClosingNode
  createClosingNode(Ruby ruby, String uri, String localName, String qName, int depth, Stack<String> langStack,
                    Stack<String> xmlBaseStack)
  {
    return new ClosingNode(ruby, uri, localName, qName, depth, langStack, xmlBaseStack);
  }

  public static class ClosingNode extends ReaderNode
  {

    // public ClosingNode() {}

    ClosingNode(Ruby runtime, String uri, String localName, String qName, int depth, Stack<String> langStack,
                Stack<String> xmlBaseStack)
    {
      super(runtime);
      nodeType = ReaderNodeType.END_ELEMENT.getValue();
      this.uri = "".equals(uri) ? null : uri;
      this.localName = ! isBlank(localName) ? localName : qName;
      this.name = qName;
      parsePrefix(qName);
      this.depth = depth;
      if (!langStack.isEmpty()) { this.lang = langStack.peek(); }
      if (!xmlBaseStack.isEmpty()) { this.xmlBase = xmlBaseStack.peek(); }
    }

    @Override
    public IRubyObject
    getAttributeCount()
    {
      return ruby.newFixnum(0);
    }

    @Override
    public RubyBoolean
    hasValue()
    {
      return ruby.getFalse();
    }

    @Override
    public String
    getString()
    {
      return "</" + name + '>';
    }
  }

  public static ElementNode
  createElementNode(Ruby ruby, String uri, String localName, String qName, XMLAttributes attrs, int depth,
                    Stack<String> langStack, Stack<String> xmlBaseStack)
  {
    return new ElementNode(ruby, uri, localName, qName, attrs, depth, langStack, xmlBaseStack);
  }

  public static class ElementNode extends ReaderNode
  {

    // public ElementNode() {}

    ElementNode(Ruby runtime, String uri, String localName, String qName, XMLAttributes attrs, int depth,
                Stack<String> langStack, Stack<String> xmlBaseStack)
    {
      super(runtime);
      this.nodeType = ReaderNodeType.ELEMENT.getValue();
      this.uri = "".equals(uri) ? null : uri;
      this.localName = ! isBlank(localName) ? localName : qName;
      this.name = qName;
      parsePrefix(qName);
      this.depth = depth;
      parseAttributes(attrs, langStack, xmlBaseStack);
    }

    @Override
    public RubyBoolean
    hasValue()
    {
      return ruby.getFalse();
    }

    private void
    parseAttributes(XMLAttributes attrs, Stack<String> langStack, Stack<String> xmlBaseStack)
    {
      if (attrs.getLength() > 0) { attributeList = new ReaderAttributeList(); }
      String u, n, v;
      for (int i = 0; i < attrs.getLength(); i++) {
        u = attrs.getURI(i);
        n = attrs.getQName(i);
        v = attrs.getValue(i);
        if (isNamespace(n)) {
          if (namespaces == null) { namespaces = new HashMap<String, String>(); }
          namespaces.put(n, v);
        } else {
          if (lang == null) { lang = resolveLang(n, v, langStack); }
          if (xmlBase == null) { xmlBase = resolveXmlBase(n, v, xmlBaseStack); }
        }
        attributeList.add(u, n, v);
      }
    }

    private String
    resolveLang(String n, String v, Stack<String> langStack)
    {
      if ("xml:lang".equals(n)) {
        return v;
      } else if (!langStack.isEmpty()) {
        return langStack.peek();
      } else {
        return null;
      }
    }

    private String
    resolveXmlBase(String n, String v, Stack<String> xmlBaseStack)
    {
      if (isXmlBase(n)) {
        return getXmlBaseUri(n, v, xmlBaseStack);
      } else if (!xmlBaseStack.isEmpty()) {
        return xmlBaseStack.peek();
      } else {
        return null;
      }
    }

    private String
    getXmlBaseUri(String n, String v, Stack<String> xmlBaseStack)
    {
      if ("xml:base".equals(n)) {
        if (v.startsWith("http://")) {
          return v;
        } else if (v.startsWith("/") && v.endsWith("/")) {
          String sub = v.substring(1, v.length() - 2);
          String base = xmlBaseStack.peek();
          if (base.endsWith("/")) {
            base = base.substring(0, base.length() - 1);
          }
          int pos = base.lastIndexOf("/");
          return base.substring(0, pos).concat(sub);
        } else {
          String base = xmlBaseStack.peek();
          if (base.endsWith("/")) { return base.concat(v); }
          else { return base.concat("/").concat(v); }
        }
      } else if ("xlink:href".equals(n)) {
        if (v.startsWith("http://")) {
          return v;
        } else if (!xmlBaseStack.isEmpty()) {
          String base = xmlBaseStack.peek();
          return base;
        }
      }
      return null;
    }

    @Override
    public String
    getString()
    {
      StringBuffer sb = new StringBuffer(24);
      sb.append('<').append(name);
      if (attributeList != null) {
        for (int i = 0; i < attributeList.length; i++) {
          String n = attributeList.names.get(i);
          String v = attributeList.values.get(i);
          sb.append(' ').append(n).append('=')
          .append('"').append(v).append('"');
        }
      }
      if (hasChildren) { sb.append('>'); }
      else { sb.append("/>"); }
      return sb.toString();
    }
  }

  private static class ReaderAttributeList
  {
    final List<String> namespaces  = new ArrayList<String>();
    final List<String> names  = new ArrayList<String>();
    final List<String> values = new ArrayList<String>();
    int length = 0;

    void
    add(String namespace, String name, String value)
    {
      namespaces.add(namespace != null ? namespace : "");
      names.add(name != null ? name : "");
      values.add(value != null ? value : "");
      length++;
    }

    String
    getByName(String name)
    {
      for (int i = 0; i < names.size(); i++) {
        if (name.equals(names.get(i))) {
          return values.get(i);
        }
      }
      return null;
    }
  }

  public static class EmptyNode extends ReaderNode
  {

    public
    EmptyNode(Ruby runtime)
    {
      super(runtime);
      this.nodeType = ReaderNodeType.NODE.getValue();
    }

    @Override
    public IRubyObject
    getXmlVersion()
    {
      return this.ruby.getNil();
    }

    @Override
    public RubyBoolean
    hasValue()
    {
      return ruby.getFalse();
    }

    @Override
    public String
    getString()
    {
      return null;
    }
  }

  public static class ExceptionNode extends EmptyNode
  {
    private final XmlSyntaxError exception;

    public
    ExceptionNode(Ruby runtime, Exception ex)
    {
      super(runtime);
      exception = XmlSyntaxError.createXMLSyntaxError(runtime); // Nokogiri::XML::SyntaxError
      exception.setException(ex);
    }

    @Override
    public boolean
    isError()
    {
      return true;
    }

    @Override
    public IRubyObject
    toSyntaxError()
    {
      return this.exception;
    }
  }

  public static TextNode
  createTextNode(Ruby ruby, String content, int depth, Stack<String> langStack, Stack<String> xmlBaseStack)
  {
    return new TextNode(ruby, content, depth, langStack, xmlBaseStack);
  }

  public static class TextNode extends ReaderNode
  {

    // public TextNode() {}

    TextNode(Ruby runtime, String content, int depth, Stack<String> langStack, Stack<String> xmlBaseStack)
    {
      super(runtime);
      this.value = content;
      this.localName = "#text";
      this.name = "#text";
      this.depth = depth;
      if (!isBlank(content)) { nodeType = ReaderNodeType.TEXT.getValue(); }
      else { nodeType = ReaderNodeType.SIGNIFICANT_WHITESPACE.getValue(); }
      if (!langStack.isEmpty()) { this.lang = langStack.peek(); }
      if (!xmlBaseStack.isEmpty()) { this.xmlBase = xmlBaseStack.peek(); }
    }

    @Override
    public RubyBoolean
    hasValue()
    {
      return ruby.getTrue();
    }

    @Override
    public String
    getString()
    {
      return encodeJavaString(value).toString();
    }
  }

}
