#include "XLIFFFile.h"

#include <algorithm>
#include <sys/time.h>
#include <time.h>
#include <cstdio>

const char* const XLIFF_XMLNS = "urn:oasis:names:tc:xliff:document:1.2";

const char *const NS_MAP[] = {
    "", XLIFF_XMLNS,
    "xml", XMLNS_XMLNS,
    NULL, NULL
};

const XMLNamespaceMap XLIFF_NAMESPACES(NS_MAP);

int
XLIFFFile::File::Compare(const XLIFFFile::File& that) const
{
    if (filename != that.filename) {
        return filename < that.filename ? -1 : 1;
    }
    return 0;
}

// =====================================================================================
XLIFFFile::XLIFFFile()
{
}

XLIFFFile::~XLIFFFile()
{
}

static XMLNode*
get_unique_node(const XMLNode* parent, const string& ns, const string& name, bool required)
{
    size_t count = parent->CountElementsByName(ns, name);
    if (count == 1) {
        return parent->GetElementByNameAt(ns, name, 0);
    } else {
        if (required) {
            SourcePos pos = count == 0
                                ? parent->Position()
                                : parent->GetElementByNameAt(XLIFF_XMLNS, name, 1)->Position();
            pos.Error("<%s> elements must contain exactly one <%s> element",
                                parent->Name().c_str(), name.c_str());
        }
        return NULL;
    }
}

XLIFFFile*
XLIFFFile::Parse(const string& filename)
{
    XLIFFFile* result = new XLIFFFile();

    XMLNode* root = NodeHandler::ParseFile(filename, XMLNode::PRETTY);
    if (root == NULL) {
        return NULL;
    }

    // <file>
    vector<XMLNode*> files = root->GetElementsByName(XLIFF_XMLNS, "file");
    for (size_t i=0; i<files.size(); i++) {
        XMLNode* file = files[i];

        string datatype = file->GetAttribute("", "datatype", "");
        string originalFile = file->GetAttribute("", "original", "");

        Configuration sourceConfig;
        sourceConfig.locale = file->GetAttribute("", "source-language", "");
        result->m_sourceConfig = sourceConfig;

        Configuration targetConfig;
        targetConfig.locale = file->GetAttribute("", "target-language", "");
        result->m_targetConfig = targetConfig;

        result->m_currentVersion = file->GetAttribute("", "build-num", "");
        result->m_oldVersion = "old";

        // <body>
        XMLNode* body = get_unique_node(file, XLIFF_XMLNS, "body", true);
        if (body == NULL) continue;

        // <trans-unit>
        vector<XMLNode*> transUnits = body->GetElementsByName(XLIFF_XMLNS, "trans-unit");
        for (size_t j=0; j<transUnits.size(); j++) {
            XMLNode* transUnit = transUnits[j];

            string rawID = transUnit->GetAttribute("", "id", "");
            if (rawID == "") {
                transUnit->Position().Error("<trans-unit> tag requires an id");
                continue;
            }
            string id;
            int index;

            if (!StringResource::ParseTypedID(rawID, &id, &index)) {
                transUnit->Position().Error("<trans-unit> has invalid id '%s'\n", rawID.c_str());
                continue;
            }

            // <source>
            XMLNode* source = get_unique_node(transUnit, XLIFF_XMLNS, "source", false);
            if (source != NULL) {
                XMLNode* node = source->Clone();
                node->SetPrettyRecursive(XMLNode::EXACT);
                result->AddStringResource(StringResource(source->Position(), originalFile,
                            sourceConfig, id, index, node, CURRENT_VERSION,
                            result->m_currentVersion));
            }

            // <target>
            XMLNode* target = get_unique_node(transUnit, XLIFF_XMLNS, "target", false);
            if (target != NULL) {
                XMLNode* node = target->Clone();
                node->SetPrettyRecursive(XMLNode::EXACT);
                result->AddStringResource(StringResource(target->Position(), originalFile,
                            targetConfig, id, index, node, CURRENT_VERSION,
                            result->m_currentVersion));
            }

            // <alt-trans>
            XMLNode* altTrans = get_unique_node(transUnit, XLIFF_XMLNS, "alt-trans", false);
            if (altTrans != NULL) {
                // <source>
                XMLNode* altSource = get_unique_node(altTrans, XLIFF_XMLNS, "source", false);
                if (altSource != NULL) {
                    XMLNode* node = altSource->Clone();
                    node->SetPrettyRecursive(XMLNode::EXACT);
                    result->AddStringResource(StringResource(altSource->Position(),
                                originalFile, sourceConfig, id, index, node, OLD_VERSION,
                                result->m_oldVersion));
                }

                // <target>
                XMLNode* altTarget = get_unique_node(altTrans, XLIFF_XMLNS, "target", false);
                if (altTarget != NULL) {
                    XMLNode* node = altTarget->Clone();
                    node->SetPrettyRecursive(XMLNode::EXACT);
                    result->AddStringResource(StringResource(altTarget->Position(),
                                originalFile, targetConfig, id, index, node, OLD_VERSION,
                                result->m_oldVersion));
                }
            }
        }
    }
    delete root;
    return result;
}

XLIFFFile*
XLIFFFile::Create(const Configuration& sourceConfig, const Configuration& targetConfig,
                                const string& currentVersion)
{
    XLIFFFile* result = new XLIFFFile();
        result->m_sourceConfig = sourceConfig;
        result->m_targetConfig = targetConfig;
        result->m_currentVersion = currentVersion;
    return result;
}

set<string>
XLIFFFile::Files() const
{
    set<string> result;
    for (vector<File>::const_iterator f = m_files.begin(); f != m_files.end(); f++) {
        result.insert(f->filename);
    }
    return result;
}

void
XLIFFFile::AddStringResource(const StringResource& str)
{
    string id = str.TypedID();

    File* f = NULL;
    const size_t I = m_files.size();
    for (size_t i=0; i<I; i++) {
        if (m_files[i].filename == str.file) {
            f = &m_files[i];
            break;
        }
    }
    if (f == NULL) {
        File file;
        file.filename = str.file;
        m_files.push_back(file);
        f = &m_files[I];
    }

    const size_t J = f->transUnits.size();
    TransUnit* g = NULL;
    for (size_t j=0; j<J; j++) {
        if (f->transUnits[j].id == id) {
            g = &f->transUnits[j];
        }
    }
    if (g == NULL) {
        TransUnit group;
        group.id = id;
        f->transUnits.push_back(group);
        g = &f->transUnits[J];
    }

    StringResource* res = find_string_res(*g, str);
    if (res == NULL) {
        return ;
    }
    if (res->id != "") {
        str.pos.Error("Duplicate string resource: %s", res->id.c_str());
        res->pos.Error("Previous definition here");
        return ;
    }
    *res = str;

    m_strings.insert(str);
}

void
XLIFFFile::Filter(bool (*func)(const string&,const TransUnit&,void*), void* cookie)
{
    const size_t I = m_files.size();
    for (size_t ix=0, i=I-1; ix<I; ix++, i--) {
        File& file = m_files[i];

        const size_t J = file.transUnits.size();
        for (size_t jx=0, j=J-1; jx<J; jx++, j--) {
            TransUnit& tu = file.transUnits[j];

            bool keep = func(file.filename, tu, cookie);
            if (!keep) {
                if (tu.source.id != "") {
                    m_strings.erase(tu.source);
                }
                if (tu.target.id != "") {
                    m_strings.erase(tu.target);
                }
                if (tu.altSource.id != "") {
                    m_strings.erase(tu.altSource);
                }
                if (tu.altTarget.id != "") {
                    m_strings.erase(tu.altTarget);
                }
                file.transUnits.erase(file.transUnits.begin()+j);
            }
        }
        if (file.transUnits.size() == 0) {
            m_files.erase(m_files.begin()+i);
        }
    }
}

void
XLIFFFile::Map(void (*func)(const string&,TransUnit*,void*), void* cookie)
{
    const size_t I = m_files.size();
    for (size_t i=0; i<I; i++) {
        File& file = m_files[i];

        const size_t J = file.transUnits.size();
        for (size_t j=0; j<J; j++) {
            func(file.filename, &(file.transUnits[j]), cookie);
        }
    }
}

TransUnit*
XLIFFFile::EditTransUnit(const string& filename, const string& id)
{
    const size_t I = m_files.size();
    for (size_t ix=0, i=I-1; ix<I; ix++, i--) {
        File& file = m_files[i];
        if (file.filename == filename) {
            const size_t J = file.transUnits.size();
            for (size_t jx=0, j=J-1; jx<J; jx++, j--) {
                TransUnit& tu = file.transUnits[j];
                if (tu.id == id) {
                    return &tu;
                }
            }
        }
    }
    return NULL;
}

StringResource*
XLIFFFile::find_string_res(TransUnit& g, const StringResource& str)
{
    int index;
    if (str.version == CURRENT_VERSION) {
        index = 0;
    }
    else if (str.version == OLD_VERSION) {
        index = 2;
    }
    else {
        str.pos.Error("Internal Error %s:%d\n", __FILE__, __LINE__);
        return NULL;
    }
    if (str.config == m_sourceConfig) {
        // index += 0;
    }
    else if (str.config == m_targetConfig) {
        index += 1;
    }
    else {
        str.pos.Error("unknown config for string %s: %s", str.id.c_str(),
                            str.config.ToString().c_str());
        return NULL;
    }
    switch (index) {
        case 0:
            return &g.source;
        case 1:
            return &g.target;
        case 2:
            return &g.altSource;
        case 3:
            return &g.altTarget;
    }
    str.pos.Error("Internal Error %s:%d\n", __FILE__, __LINE__);
    return NULL;
}

int
convert_html_to_xliff(const XMLNode* original, const string& name, XMLNode* addTo, int* phID)
{
    int err = 0;
    if (original->Type() == XMLNode::TEXT) {
        addTo->EditChildren().push_back(original->Clone());
        return 0;
    } else {
        string ctype;
        if (original->Namespace() == "") {
            if (original->Name() == "b") {
                ctype = "bold";
            }
            else if (original->Name() == "i") {
                ctype = "italic";
            }
            else if (original->Name() == "u") {
                ctype = "underline";
            }
        }
        if (ctype != "") {
            vector<XMLAttribute> attrs;
            attrs.push_back(XMLAttribute(XLIFF_XMLNS, "ctype", ctype));
            XMLNode* copy = XMLNode::NewElement(original->Position(), XLIFF_XMLNS, "g",
                                                attrs, XMLNode::EXACT);

            const vector<XMLNode*>& children = original->Children();
            size_t I = children.size();
            for (size_t i=0; i<I; i++) {
                err |= convert_html_to_xliff(children[i], name, copy, phID);
            }
            return err;
        }
        else {
            if (original->Namespace() == XLIFF_XMLNS) {
                addTo->EditChildren().push_back(original->Clone());
                return 0;
            } else {
                if (original->Namespace() == "") {
                    // flatten out the tag into ph tags -- but only if there is no namespace
                    // that's still unsupported because propagating the xmlns attribute is hard.
                    vector<XMLAttribute> attrs;
                    char idStr[30];
                    (*phID)++;
                    sprintf(idStr, "id-%d", *phID);
                    attrs.push_back(XMLAttribute(XLIFF_XMLNS, "id", idStr));

                    if (original->Children().size() == 0) {
                        XMLNode* ph = XMLNode::NewElement(original->Position(), XLIFF_XMLNS,
                                "ph", attrs, XMLNode::EXACT);
                        ph->EditChildren().push_back(
                                XMLNode::NewText(original->Position(),
                                    original->ToString(XLIFF_NAMESPACES),
                                    XMLNode::EXACT));
                        addTo->EditChildren().push_back(ph);
                    } else {
                        XMLNode* begin = XMLNode::NewElement(original->Position(), XLIFF_XMLNS,
                                "bpt", attrs, XMLNode::EXACT);
                        begin->EditChildren().push_back(
                                XMLNode::NewText(original->Position(),
                                    original->OpenTagToString(XLIFF_NAMESPACES, XMLNode::EXACT),
                                    XMLNode::EXACT));
                        XMLNode* end = XMLNode::NewElement(original->Position(), XLIFF_XMLNS,
                                "ept", attrs, XMLNode::EXACT);
                        string endText = "</";
                            endText += original->Name();
                            endText += ">";
                        end->EditChildren().push_back(XMLNode::NewText(original->Position(),
                                endText, XMLNode::EXACT));

                        addTo->EditChildren().push_back(begin);

                        const vector<XMLNode*>& children = original->Children();
                        size_t I = children.size();
                        for (size_t i=0; i<I; i++) {
                            err |= convert_html_to_xliff(children[i], name, addTo, phID);
                        }

                        addTo->EditChildren().push_back(end);
                    }
                    return err;
                } else {
                    original->Position().Error("invalid <%s> element in <%s> tag\n",
                                                original->Name().c_str(), name.c_str());
                    return 1;
                }
            }
        }
    }
}

XMLNode*
create_string_node(const StringResource& str, const string& name)
{
    vector<XMLAttribute> attrs;
    attrs.push_back(XMLAttribute(XMLNS_XMLNS, "space", "preserve"));
    XMLNode* node = XMLNode::NewElement(str.pos, XLIFF_XMLNS, name, attrs, XMLNode::EXACT);

    const vector<XMLNode*>& children = str.value->Children();
    size_t I = children.size();
    int err = 0;
    for (size_t i=0; i<I; i++) {
        int phID = 0;
        err |= convert_html_to_xliff(children[i], name, node, &phID);
    }

    if (err != 0) {
        delete node;
    }
    return node;
}

static bool
compare_id(const TransUnit& lhs, const TransUnit& rhs)
{
    string lid, rid;
    int lindex, rindex;
    StringResource::ParseTypedID(lhs.id, &lid, &lindex);
    StringResource::ParseTypedID(rhs.id, &rid, &rindex);
    if (lid < rid) return true;
    if (lid == rid && lindex < rindex) return true;
    return false;
}

XMLNode*
XLIFFFile::ToXMLNode() const
{
    XMLNode* root;
    size_t N;

    // <xliff>
    {
        vector<XMLAttribute> attrs;
        XLIFF_NAMESPACES.AddToAttributes(&attrs);
        attrs.push_back(XMLAttribute(XLIFF_XMLNS, "version", "1.2"));
        root = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, "xliff", attrs, XMLNode::PRETTY);
    }

    vector<TransUnit> groups;

    // <file>
    vector<File> files = m_files;
    sort(files.begin(), files.end());
    const size_t I = files.size();
    for (size_t i=0; i<I; i++) {
        const File& file = files[i];

        vector<XMLAttribute> fileAttrs;
        fileAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "datatype", "x-android-res"));
        fileAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "original", file.filename));

        struct timeval tv;
        struct timezone tz;
        gettimeofday(&tv, &tz);
        fileAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "date", trim_string(ctime(&tv.tv_sec))));

        fileAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "source-language", m_sourceConfig.locale));
        fileAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "target-language", m_targetConfig.locale));
        fileAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "build-num", m_currentVersion));

        XMLNode* fileNode = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, "file", fileAttrs,
                                                XMLNode::PRETTY);
        root->EditChildren().push_back(fileNode);

        // <body>
        XMLNode* bodyNode = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, "body",
                                                vector<XMLAttribute>(), XMLNode::PRETTY);
        fileNode->EditChildren().push_back(bodyNode);

        // <trans-unit>
        vector<TransUnit> transUnits = file.transUnits;
        sort(transUnits.begin(), transUnits.end(), compare_id);
        const size_t J = transUnits.size();
        for (size_t j=0; j<J; j++) {
            const TransUnit& transUnit = transUnits[j];

            vector<XMLAttribute> tuAttrs;

            // strings start with string:
            tuAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "id", transUnit.id));
            XMLNode* transUnitNode = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, "trans-unit",
                                                         tuAttrs, XMLNode::PRETTY);
            bodyNode->EditChildren().push_back(transUnitNode);

            // <extradata>
            if (transUnit.source.comment != "") {
                vector<XMLAttribute> extradataAttrs;
                XMLNode* extraNode = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, "extradata",
                                                            extradataAttrs, XMLNode::EXACT);
                transUnitNode->EditChildren().push_back(extraNode);
                extraNode->EditChildren().push_back(
                        XMLNode::NewText(GENERATED_POS, transUnit.source.comment,
                                         XMLNode::PRETTY));
            }

            // <source>
            if (transUnit.source.id != "") {
                transUnitNode->EditChildren().push_back(
                                    create_string_node(transUnit.source, "source"));
            }
            
            // <target>
            if (transUnit.target.id != "") {
                transUnitNode->EditChildren().push_back(
                                    create_string_node(transUnit.target, "target"));
            }

            // <alt-trans>
            if (transUnit.altSource.id != "" || transUnit.altTarget.id != ""
                    || transUnit.rejectComment != "") {
                vector<XMLAttribute> altTransAttrs;
                XMLNode* altTransNode = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, "alt-trans",
                                                            altTransAttrs, XMLNode::PRETTY);
                transUnitNode->EditChildren().push_back(altTransNode);

                // <extradata>
                if (transUnit.rejectComment != "") {
                    vector<XMLAttribute> extradataAttrs;
                    XMLNode* extraNode = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS,
                                                                "extradata", extradataAttrs,
                                                                XMLNode::EXACT);
                    altTransNode->EditChildren().push_back(extraNode);
                    extraNode->EditChildren().push_back(
                            XMLNode::NewText(GENERATED_POS, transUnit.rejectComment,
                                             XMLNode::PRETTY));
                }
                
                // <source>
                if (transUnit.altSource.id != "") {
                    altTransNode->EditChildren().push_back(
                                        create_string_node(transUnit.altSource, "source"));
                }
                
                // <target>
                if (transUnit.altTarget.id != "") {
                    altTransNode->EditChildren().push_back(
                                        create_string_node(transUnit.altTarget, "target"));
                }
            }
            
        }
    }

    return root;
}


string
XLIFFFile::ToString() const
{
    XMLNode* xml = ToXMLNode();
    string s = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
    s += xml->ToString(XLIFF_NAMESPACES);
    delete xml;
    s += '\n';
    return s;
}

Stats
XLIFFFile::GetStats(const string& config) const
{
    Stats stat;
    stat.config = config;
    stat.files = m_files.size();
    stat.toBeTranslated = 0;
    stat.noComments = 0;

    for (vector<File>::const_iterator file=m_files.begin(); file!=m_files.end(); file++) {
        stat.toBeTranslated += file->transUnits.size();

        for (vector<TransUnit>::const_iterator tu=file->transUnits.begin();
                    tu!=file->transUnits.end(); tu++) {
            if (tu->source.comment == "") {
                stat.noComments++;
            }
        }
    }

    stat.totalStrings = stat.toBeTranslated;

    return stat;
}