
package ixbrl

// See - http://d.hatena.ne.jp/kmizushima/20100223/1266940545
//      > import scala.collection.mutable.{Seq => _, _}
import scala.collection.mutable.{Seq => _, _}

import java.net.{URL}
import scala.xml._
import scala.xml.PrettyPrinter
import java.io.FileOutputStream

object Extractor {

    val footnoteInfoID2Froms = Map[String, List[String]]() // footnoteID -> List[ID of Elem]
    var myTopNS:NamespaceBinding = TopScope

    def readHtmlFile(file:String):Elem = { XML.loadFile(file) }
    def readHtmlURL(urlStr:String):Elem = {
        val url = new URL(urlStr)
        val conn = url.openConnection
        XML.load(conn.getInputStream)
    }
    def prittyString(xml:Node) = {
        Canonicalizer.canonicalize(new PrettyPrinter(72 /*width*/, 2 /*indent*/).format(xml))
        // new PrettyPrinter(72 /*width*/, 2 /*indent*/).format(xml)
    }

    def saveFile(xml:Node, filepath:String) = XML.saveFull(filepath, xml, "UTF-8", true, null)
    def openOutFile(filePath: String) (block: FileOutputStream => Unit) {
        val out = new FileOutputStream(filePath)
        try {
            block(out)
        } finally {
            out.close()
        }
    }

    // html の Namespace, inlinexbrl の Namespaceを削除した Scope を作成する
    def generateTopNS(ns:NamespaceBinding):NamespaceBinding = {
        myTopNS = generateNS(ns)
        myTopNS
    }

    def generateNS(ns:NamespaceBinding):NamespaceBinding = {
        if (ns == null) { TopScope }
        else if (ns == TopScope) { TopScope }
        else if (ns.uri == "http://www.w3.org/1999/xhtml") { generateNS(ns.parent) }
        else if (ns.uri == "http://www.xbrl.org/2008/inlineXBR") { generateNS(ns.parent) }
        else if (ns.uri == "http://www.xbrl.org/2008/inlineXBRL/transformation") { generateNS(ns.parent) }
        else { new NamespaceBinding(ns.prefix, ns.uri, generateNS(ns.parent)) }
    }

    def collectTargets(doc:Node): Set[String] = {
        collectTargetsSub(doc) + ""
    }
    def collectTargetsSub(node:Node): Set[String] = {
        val ans = HashSet[String]()
        node match {
            case Elem(prefix, label, attrs, scop, childlen @ _*) =>
                if (attrs("target") != null) { ans += attrs("target").toString }
                childlen foreach { ch => ans ++= collectTargetsSub(ch) }
            case _ =>
        }
        ans
    }

    def getPrefixs(attrs:MetaData):Seq[String] = {
        val ans = new Queue[String]()
        if (attrs == null) {}
        else if (attrs.isPrefixed) { ans += (attrs.asInstanceOf[PrefixedAttribute].pre) }
        else { ans ++= getPrefixs(attrs.next) }
        ans
    }
    def getAttrsScope(scope:NamespaceBinding, attrs:MetaData):NamespaceBinding = {
        if (attrs == null) {
            TopScope
        } else {
            val parentNS = getAttrsScope(scope, attrs.next)
            var ans = parentNS
            if (attrs.isPrefixed) {
                val pa = attrs.asInstanceOf[PrefixedAttribute]
                val pre = pa.pre
                if ((myTopNS.getURI(pre) == null) && (parentNS.getURI(pre) == null)) {
                    ans = new NamespaceBinding(pre, scope.getURI(pre), parentNS)
                }
            }
            ans
        }
    }

    // Node の Scope を整理する, @target を削除する
    def fixSeq(ns: Seq[Node]): Seq[Node] = fixSeq(ns, false)
    def fixSeq(ns: Seq[Node], target_p:Boolean): Seq[Node] = for(node <- ns) yield node match {
        case Elem(prefix, label, attrs, scope, children @ _*) =>
            val chs = fixSeq(children, target_p)
            val scop = {
                val attrScop = getAttrsScope(scope, attrs)
                if (myTopNS.getURI(prefix) != null) attrScop
                else  new NamespaceBinding(prefix, scope.getURI(prefix), attrScop)
            }
            Elem(prefix, label,
                 if (target_p) attrs.remove("target") else attrs,
                 scop, chs : _*)
        case other => other
    }

    def excludes(ns: Seq[Node]): Seq[Node] = {
        val ans = new Queue[Node]
        for(n <- ns) {
            n match {
                case Elem("ix", "exclude", attrs, scope, children @ _*) =>
                case other => ans += exclude(other)
            }
        }
        ans
    }
    def exclude(node:Node):Node = {
        node match {
            case Elem(prefix, label, attrs, scope, children @ _*) =>
                Elem(prefix, label, attrs, scope, excludes(children) : _*)
            case other => node
        }
    }

    def escape(ns: Seq[Node]): Seq[Node] = {
        val ns2 = fixSeq(ns)
        val ans = new Queue[Node]
        ans += Text(ns2.toArray.mkString)
        ans
    }

    def formatted(format:String, ns: Seq[Node]): Seq[Node] = {
        val ns2 = fixSeq(ns)
        val ans = new Queue[Node]
        for(n <- ns) {
            n match {
                case Text(s) => ans += Text(Transformation.transform(format,s))
                case other => ans += formattedNode(format, n)
            }
        }
        ans
    }
    def formattedNode(format:String, node: Node): Node = {
        node match {
            case Text(s) => Text(Transformation.transform(format,s))
            case Elem(prefix, label, attrs, scope, children @ _*) => formatted(format, children).first
            case _ => Text("")
        }
    }

    def getItemValueStr(n:Node): String = {
        val sign = n match {
            case Elem(prefix, label, attrs, scope, children @ _*) =>
                if (n.attributes("sign") == null) 1
                else -1
            case _ => 1
        }
        val scale = n match {
            case Elem(prefix, label, attrs, scope, children @ _*) =>
                if (n.attributes("scale") == null) 0
                else Integer.parseInt(n.attributes("scale").toString)
            case _ => 0
        }
        val formattedStr = (if (n.attributes("format") == null) n.child.first.toString
                            else Transformation.transform(n.attributes("format").toString,
                                                          n.child.first.toString))
        val v = sign * java.lang.Double.parseDouble(formattedStr) * Math.pow(10, scale)
        if (Math.round(v) == v) {
            String.format("%d", long2Long(Math.round(v)))
        } else {
            val s = String.format("%f", double2Double(v))
            val valPattern = """(.*\.[1-9]*)(0*)""".r
            s match {
                case valPattern(s1, z0) => s1
                case _ => s
            }
        }
    }

    // nonFraction item を生成する
    def makeItem(n:Node):Node = {
        n match {
            case Elem(prefix, label, attrs, scope, children @ _*) => {
                    val nameAttr = attrs("name").toString
                    val namePattern = """(.*):(.*)""".r
                    nameAttr match {
                        case namePattern(prefix2, label2) =>
                            if (label == "nonFraction") {
                                Elem(prefix2, label2,
                                     attrs.remove("name").remove("scale").remove("format").remove("sign").remove("footnoteRefs"),
                                     scope, Text(getItemValueStr(n)))
                            } else {
                                Elem(prefix2, label2,
                                     attrs.remove("name").remove("format").remove("footnoteRefs"),
                                     scope, makeFractionItem(n) : _*)
                            }
                        case _ =>
                            Elem(null, nameAttr, attrs.remove("name").remove("footnoteRefs"), scope, fixSeq(children, true) : _*)

                    }
                }
            case other => other
        }
    }

    // Fraction item を生成する
    def makeFractionItem(n:Node):Seq[Node] = {
        val ans = new Queue[Node]()
        n.child foreach {ch =>
            ch match {
                case Elem("ix", "numerator", attrs, scope, children @ _*) =>
                    ans ++= Elem("xbrli", ch.label, ch.attributes.remove("scale").remove("format").remove("sign"),
                                 scope, Text(getItemValueStr(ch)))
                case Elem("ix", "denominator", attrs, scope, children @ _*) =>
                    ans ++= Elem("xbrli", ch.label, ch.attributes.remove("scale").remove("format").remove("sign"),
                                 scope, Text(getItemValueStr(ch)))
                case other => ans ++= Text(" ")
            }
        }
        ans
    }

    // NonNumeric を生成する
    def makeNonNumeric(n:Node):Node = {
        n match {
            case Elem(prefix, label, attrs, scope, children @ _*) => {
                    val nameAttr = attrs("name").toString
                    val namePattern = """(.*):(.*)""".r
                    val childrenEx = excludes(children)
                    nameAttr match {
                        case namePattern(prefix2, label2) =>
                            if (attrs("format") != null) {
                                Elem(prefix2, label2, attrs.remove("name").remove("format").remove("escape").remove("footnoteRefs"),
                                     scope, fixSeq(formatted(attrs("format").toString,
                                                             childrenEx)) : _*)
                            } else if (attrs("escape") != null) {
                                Elem(prefix2, label2, attrs.remove("name").remove("escape").remove("footnoteRefs").remove("target"),
                                     scope, fixSeq(escape(childrenEx)) : _*)
                            } else {
                                Elem(prefix2, label2, attrs.remove("name").remove("footnoteRefs").remove("target"),
                                     scope, fixSeq(childrenEx) : _*)
                            }
                        case _ =>
                            Elem(null, nameAttr, attrs.remove("name").remove("footnoteRefs"), scope, fixSeq(childrenEx) : _*)
                    }
                }
            case other => other
        }
    }
    // Tuple を生成する
    def makeTuple(node:Node):Node = {
        val nameAttr = node.attributes("name").toString
        val namePattern = """(.*):(.*)""".r
        nameAttr match {
            case namePattern(prefix2, label2) =>
                val chs = makeTupleSub(node.child)
                Elem(prefix2, label2,
                     node.attributes.remove("name").remove("tupleID").remove("target"),
                     node.scope, chs : _*)
            case _ => node
        }
    }

    def makeTupleSub(ns: Seq[Node]): Seq[Node] = {
        val ans = new ArrayBuffer[Node]

        ns foreach { n =>
            if (n.attributes("order") != null) {
                val order = Integer.parseInt(n.attributes("order").toString)
                val count = order - ans.length
                for ( i <- 0 until count) ans += null

                val t = makeNonNumeric(n)
                ans(order - 1) = Elem(t.prefix, t.label, t.attributes.remove("order"),
                                      t.scope, t.child :_*)
            }
        }
        ans
    }

    def makeFootnote(node:Node): Seq[Node] = {
        val ans = new ArrayBuffer[Node]

        val lang = node.attributes.get(node.scope.getURI("xml"), node.scope,"lang").get.toString
        val footnoteID = node.attributes("footnoteID").toString
        val elemID = footnoteInfoID2Froms(footnoteID).first
        val arcrole = (if (node.attributes("arcrole") == null) "http://www.xbrl.org/2003/arcrole/fact-footnote"
                       else node.attributes("arcrole").toString)
        val footnoteRole = (if (node.attributes("footnoteRole") == null) "http://www.xbrl.org/2003/role/footnote"
                            else node.attributes("footnoteRole").toString)
        val locAttrs = Attr.makeAttrs(new Attr("xlink",  "href", "#" + elemID),
                                      new Attr("xlink",  "label", elemID),
                                      new Attr("xlink",  "type",  "locator"))
        val arcAttrs = Attr.makeAttrs(new Attr("xlink",  "from",  elemID),
                                      new Attr("xlink",  "to",    footnoteID),
                                      new Attr("xlink",  "arcrole", arcrole),
                                      new Attr("xlink",  "type",  "arc"))
        val attrFoot = List(new Attr("xlink", "role",  footnoteRole),
                            new Attr("xlink", "type",  "resource"),
                            new Attr("xlink", "label", footnoteID),
                            new Attr("xml",   "lang",  lang))

        val attrFoot2 = (if (node.attributes("title") == null) attrFoot
                         else new Attr("xlink", "title", node.attributes("title").toString)::attrFoot)
        val noteAttrs = Attr.makeAttrs(attrFoot2 :_*)

        ans += Elem("link", "loc", locAttrs, TopScope)
        ans += Elem("link", "footnoteArc", arcAttrs, TopScope)
        ans += Elem("link", footnoteID, noteAttrs, TopScope, node.child :_*)
        ans
    }

    def getTarget(node:Node):String = {
        getTargetFromAttrs(node.attributes)
    }
    def getTargetFromAttrs(attrs:MetaData):String = {
        if (attrs("target") == null) ""
        else attrs("target").toString
    }
    def initInfo(targets:Set[String]):Map[(String,String), Seq[Node]] = {
        val ans = HashMap[(String,String), Seq[Node]]()
        ans(("references",""))= Seq[Node]()
        ans(("context","")) = Seq[Node]()
        ans(("unit","")) = Seq[Node]()
        ans(("item","")) = Seq[Node]()
        ans(("footnote","")) = Seq[Node]()
        ans(("arcroleRef","")) = Seq[Node]()
        ans(("roleRef","")) = Seq[Node]()

        targets foreach { target =>
            ans(("references", target))= Seq[Node]()
            ans(("context", target)) = Seq[Node]()
            ans(("unit", target)) = Seq[Node]()
            ans(("item", target)) = Seq[Node]()
            ans(("footnote", target)) = Seq[Node]()
            ans(("arcroleRef", target)) = Seq[Node]()
            ans(("roleRef", target)) = Seq[Node]()
        }
        ans
    }

    def getXBRLInfo(doc:Node, targets:Set[String]):Map[(String,String), Seq[Node]] = {
        val ans = initInfo(targets)
        doc foreach { ch =>
            ch match {
                case Elem("ix", "header", attrs, scope, children @ _*) =>
                    ch \\"references" foreach {c2 =>
                        c2.child foreach { ref =>
                            ref match {
                                case Elem(_, _, attrs, scope, children @ _*) => {
                                        val n = Elem(ref.prefix,ref.label, attrs, scope)
                                        ans(("references", getTarget(c2))) ++= fixSeq(n)
                                    }
                                case _ =>
                            }
                        }
                    }
                    // resouces, hidden
                    ch \\"context" foreach {c2 =>
                        ans(("context", getTarget(c2))) ++= fixSeq(c2, true)
                    }
                    ch \\"unit" foreach {c2 =>
                        ans(("unit", getTarget(c2))) ++= fixSeq(c2, true)
                    }
                    ch \\"arcroleRef" foreach {c2 =>
                        ans(("arcroleRef", getTarget(c2))) ++= fixSeq(c2, true)
                    }
                    ch \\"roleRef" foreach {c2 =>
                        ans(("roleRef", getTarget(ch))) ++= fixSeq(c2, true)
                    }
                    ch \\"footnote" foreach {c2 =>
                        ans(("footnote", getTarget(c2))) ++= fixSeq(c2, true)
                    }
                case Elem("ix", "nonFraction", attrs, scope, children @ _*) =>
                    val elem = fixSeq(makeItem(ch), true)
                    ans(("item", getTarget(ch))) ++= elem
                    makeFootnoteRef(ch, elem.first)
                case Elem("ix", "fraction", attrs, scope, children @ _*) =>
                    ans(("item", getTarget(ch))) ++= fixSeq(makeItem(ch), true)
                case Elem("ix", "nonNumeric", attrs, scope, children @ _*) =>
                    ans(("item", getTarget(ch))) ++= fixSeq(makeNonNumeric(ch), true)
                case Elem("ix", "tuple", attrs, scope, children @ _*) =>
                    ans(("item", getTarget(ch))) ++= fixSeq(makeTuple(ch), true)
                case Elem(pre, label, attrs, scope, children @ _*) =>
                    children.foreach {ch2 =>
                        val subInfo = getXBRLInfo(ch2,targets)
                        targets foreach { t =>
                            ans(("references",t)) ++= subInfo(("references",t))
                            ans(("context",t)) ++= subInfo(("context",t))
                            ans(("unit",t)) ++= subInfo(("unit",t))
                            ans(("item",t)) ++= subInfo(("item",t))
                            ans(("footnote",t)) ++= subInfo(("footnote",t))
                            ans(("arcroleRef",t)) ++= subInfo(("arcroleRef",t))
                            ans(("roleRef",t)) ++= subInfo(("roleRef",t))
                        }
                    }
                case _ =>
            }
        }
        ans
    }

    // 実際に参照されている context, unit だけを返す。
    def pickupContextUnit(info:Map[(String,String), Seq[Node]], target:String): Map[(String,String), Seq[Node]] = {
        val pickuped = new HashMap[(String, String), Seq[Node]]()
        pickuped(("context", target)) = Seq[Node]()
        pickuped(("unit", target)) = Seq[Node]()

        val used = Set.empty[String]
        info(("item", target)) foreach { i => i match {
                case Elem(prefix, label, attrs, scope, children @ _*) =>
                    if (i.attributes("contextRef") != null) used += (i.attributes("contextRef").toString)
                    if (i.attributes("unitRef") != null) used += (i.attributes("unitRef").toString)
                    children foreach { ch=>
                        if (ch.attributes("contextRef") != null) used += (ch.attributes("contextRef").toString)
                        if (ch.attributes("unitRef") != null) used += (ch.attributes("unitRef").toString)
                    }
                case _ =>
            }
        }

        def isUse(n:Node) = (used.contains(n.attributes("id").toString))

        info(("context", target)) foreach { i =>
            if (isUse(i)) {
                pickuped(("context", target)) ++= i
                used.dropWhile(_ == i)
            }
        }
        info(("unit", target)) foreach { i =>
            if (isUse(i)) pickuped(("unit", target)) ++= i
            used.dropWhile(_ == i)
        }

        // @target 中に無ければ、 default target 中で探す。
        if (target != "") {
            info(("context", "")) foreach { i =>
                if (isUse(i)) pickuped(("context", target)) ++= i
            }
            info(("unit", "")) foreach { i =>
                if (isUse(i)) pickuped(("unit", target)) ++= i
            }
        }
        pickuped
    }

    def makeFootnoteRef(ch:Node, elem:Node) {
        if (ch.attributes("footnoteRefs") != null) {
            val footnoteID = ch.attributes("footnoteRefs").toString
            val elemID = elem.attributes("id").toString
            if (!footnoteInfoID2Froms.contains(footnoteID)) {
                footnoteInfoID2Froms(footnoteID) = List(elemID)
            } else {
                footnoteInfoID2Froms(footnoteID) = elemID::footnoteInfoID2Froms(footnoteID)
            }
        }
    }

    def makeFootnotLinkAll(ns:Seq[Node]):Seq[Node]= {
        val ans = new Queue[Node]()
        var roles = List("http://www.xbrl.org/2003/role/link")
        ns foreach {n =>
            if (n.attributes("footnoteLinkRole") != null) {
                val role = n.attributes("footnoteLinkRole").toString
                if (roles.find(_ == role) == None) roles = role :: roles
            }
        }

        roles.reverse foreach {role =>
            val attrs = Attr.makeAttrs(new Attr("xlink", "role", role),
                                       new Attr("xlink", "type", "extended"))
            val chs = new Queue[Node]
            ns foreach {n =>
                if (n.attributes("footnoteLinkRole") != null) {
                    if (role == n.attributes("footnoteLinkRole").toString) {
                        val fs = makeFootnote(n)
                        for (f <- fs) chs += f
                    }
                } else {
                    if (role == "http://www.xbrl.org/2003/role/link") {
                        val fs = makeFootnote(n)
                        for (f <- fs) chs += f
                    }
                }
                ans += Elem("link", "footnoteLink", attrs, TopScope, chs :_*)
            }
        }
        ans
    }

    def toXBRL(doc:Node): Map[String, Node] = {
        val ans = new HashMap[String, Node]()
        val topNS = generateTopNS(doc.scope)
        val targets = collectTargets(doc)
        val info = getXBRLInfo(doc, targets)

        targets foreach { target =>
            val footnotes = makeFootnotLinkAll(info(("footnote", target)))
            val pickuped = pickupContextUnit(info, target)
            ans(target) = Elem("xbrli", "xbrl", Null, topNS,
                               (info(("references", target)) ++
                                info(("arcroleRef", target)) ++
                                info(("roleRef", target)) ++
                                info(("item", target)) ++
                                pickuped(("context", target)) ++
                                pickuped(("unit", target)) ++
                                footnotes) :_*)
        }
        ans
    }
}

object Attr {
    def makeAttrs(attrs:Attr *): MetaData = {
        val nextAttrs = (
            if (attrs.length == 1) Null
            else makeAttrs(attrs.drop(1):_*)
        )
        val a = attrs.first
        new PrefixedAttribute(a.prefix, a.key, a.value, nextAttrs)
    }
}

case class Attr(prefix:String, key:String, value:String) {
    override def toString:String = {
        prefix + ":" + key +"="+ value
    }
}