package com.limegroup.gnutella.version;

import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;

import java.util.Collections;
import java.util.List;
import java.util.LinkedList;
import java.util.Iterator;
import java.io.StringReader;
import java.io.IOException;
import java.net.URLEncoder;


import org.apache.xerces.parsers.DOMParser;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.xml.LimeXMLUtils;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.settings.ApplicationSettings;

/**
 * An abstraction for the update XML.
 * Contains the ID & timestamp of the message, as well as the list
 * of UpdateData information for individual messages.
 */
class UpdateCollection {
    
    private static final Log LOG = LogFactory.getLog(UpdateCollection.class);
    
    /**
     * The id of this UpdateCollection.
     */
    private int collectionId = Integer.MIN_VALUE;
    
    /**
     * The timestamp of this collection.
     */
    private long collectionTimestamp = -1;
    
    /**
     * The list of UpdateData's in this collection.
     */
    private List updateDataList = new LinkedList();
    
    /**
     * The list of DownloadDatas in this collection.
     */
    private List downloadDataList = new LinkedList();
    
    /**
     * Ensure that this is only created by using the factory constructor.
     */
    private UpdateCollection() {}
    
    /**
     * A string rep of the collection.
     */
    public String toString() {
        return "Update Collection, id: " + collectionId + ", timestamp: " + collectionTimestamp +
               ", data: " + updateDataList;
    }
    
    /**
     * Gets the id of this UpdateCollection.
     */
    int getId() {
        return collectionId;
    }
    
    /**
     * Gets the timestamp.
     */
    long getTimestamp() {
        return collectionTimestamp;
    }
    
    /**
     * Gets the UpdateData objects.
     */
    List getUpdateData() {
        return updateDataList;
    }
    
    /**
     * Gets all updates that have information so we can download them.
     */
    List getUpdatesWithDownloadInformation() {
        return Collections.unmodifiableList(downloadDataList);
    }
    
    /**
     * Gets the UpdateData that is relevant to us.
     * Returns null if there is no relevant update.
     */
    UpdateData getUpdateDataFor(Version currentV, String lang, boolean currentPro,
                                int currentStyle, Version currentJava) {
        UpdateData englishMatch = null;
        UpdateData exactMatch = null;
        
        // Iterate through them till we find an acceptable version.
        // Remember for the 'English' and 'Exact' match --
        // If we got an exact, use that.  Otherwise, use English.
        for(Iterator i = updateDataList.iterator(); i.hasNext(); ) {
            UpdateData next = (UpdateData)i.next();
            if(next.isAllowed(currentV, currentPro, currentStyle, currentJava)) {
                if(lang.equals(next.getLanguage())) {
                    exactMatch = next;
                    break;
                } else if("en".equals(next.getLanguage()) && englishMatch == null) {
                    englishMatch = next;
                }
            }
        }
        
        if(exactMatch == null)
            return englishMatch;
        else
            return exactMatch;
    }

    /**
     * Constructs and returns a new UpdateCollection that corresponds
     * to the elements in the XML.
     */
    static UpdateCollection create(String xml) {
        if(LOG.isTraceEnabled())
            LOG.trace("Parsing Update XML: " + xml);

        UpdateCollection collection = new UpdateCollection();
        collection.parse(xml);
        return collection;
    }
    
    /**
     * Parses the XML and fills in the data of this collection.
     */
    private void parse(String xml) {
        DOMParser parser = new DOMParser();
        InputSource is = new InputSource(new StringReader(xml));
        try {
            parser.parse(is);
        } catch(IOException ioe) {
            LOG.error("Unable to parse: " + xml, ioe);
            return;
        } catch(SAXException sax) {
            LOG.error("Unable to parse: " + xml, sax);
            return;
        }
        
        parseDocumentElement(parser.getDocument().getDocumentElement());
    }
    
    /**
     * Parses the document element.
     *
     * This requires that the element be "update" and has the attribute 'id'.
     * The 'timestamp' attribute is checked (but is optional), as are child 'msg'
     * elements.
     */
    private void parseDocumentElement(Node doc) {
        // Ensure the document element is the 'update' element.
        if(!"update".equals(doc.getNodeName()))
            return;
        
        // Parse the 'id' & 'timestamp' attributes.
        NamedNodeMap attr = doc.getAttributes();
        
        // we MUST have an id.
        String idText = getAttributeText(attr, "id");
        if(idText == null) {
            LOG.error("No id attribute.");
            return;
        }
        
        try {
            collectionId = Integer.parseInt(idText);
        } catch(NumberFormatException nfe) {
            LOG.error("Couldn't get collection id from: " + idText, nfe);
            return;
        }
        
        // Parse the optional 'timestamp' attribute.
        String timestampText = getAttributeText(attr, "timestamp");
        if(timestampText != null) {
            try {
                collectionTimestamp = Long.parseLong(timestampText);
            } catch(NumberFormatException nfe) {
                LOG.warn("Couldn't get timestamp from: " + timestampText, nfe);
            }
        }
        
        NodeList children = doc.getChildNodes();
        for(int i = 0; i < children.getLength(); i++) {
            Node child = children.item(i);
            if("msg".equals(child.getNodeName()))
                parseMsgItem(child);
        }
    }
    
    /**
     * Parses a single msg item.
     *
     * The elements this parses are:
     *      from     -- OPTIONAL (defaults to 0.0.0)
     *      to       -- OPTIONAL (defaults to the 'for' value)
     *      for      -- REQUIRED
     *      pro      -- OPTIONAL (see free)
     *      free     -- OPTIONAL (if both pro & free are missing, both default to true.
                                  otherwise they defaults to false.  any non-null value == true)
     *      url      -- REQUIRED
     *      style    -- REQUIRED (accepts a number only.)
     *      javafrom -- OPTIONAL (see javato)
     *      javato   -- OPTIONAL (if both are missing, all ranges are valid.  if one is missing, defaults to above or below that.)
     *      os       -- OPTIONAL (defaults to '*' -- accepts a comma delimited list.)
     *
     * The below elements are necessary for downloading the update in the network.
     *      urn      -- The BITPRINT of the download
     *      ucommand -- The command to run to invoke the update.
     *      uname    -- The filename on disk the update should have.
     *      size     -- The size of the update when completed.
     *
     * If any values exist but error while parsing, the entire block is considered
     * invalid and ignored.
     */
    private void parseMsgItem(Node msg) {
        UpdateData data = new UpdateData();
        
        NamedNodeMap attr = msg.getAttributes();
        String fromV = getAttributeText(attr, "from");
        String toV = getAttributeText(attr, "to");
        String forV = getAttributeText(attr, "for");
        String pro = getAttributeText(attr, "pro");
        String free = getAttributeText(attr, "free");
        String url = getAttributeText(attr, "url");
        String style = getAttributeText(attr, "style");
        String javaFrom = getAttributeText(attr, "javafrom");
        String javaTo = getAttributeText(attr, "javato");
        String os = getAttributeText(attr, "os");
        String updateURN = getAttributeText(attr, "urn");
        String updateCommand = getAttributeText(attr, "ucommand");
        String updateName = getAttributeText(attr, "uname");
        String fileSize = getAttributeText(attr, "size");
        
        if(updateURN != null) {
            try {
                URN urn = URN.createSHA1Urn(updateURN);
                String tt = URN.getTigerTreeRoot(updateURN);
                data.setUpdateURN(urn);
                data.setUpdateTTRoot(tt);
            } catch(IOException ignored) {
                LOG.warn("Invalid bitprint urn: " + updateURN, ignored);
            }
        }
        
        data.setUpdateCommand(updateCommand);
        data.setUpdateFileName(updateName);
        
        if(fileSize != null) {
            try {
                data.setUpdateSize(Integer.parseInt(fileSize));
            } catch(NumberFormatException nfe) {
                LOG.warn("Invalid size: " + fileSize);
            }
        }
        
        // if this has enough information for downloading, add it to the list of potentials.
        if(data.getUpdateURN() != null && data.getUpdateFileName() != null && data.getSize() != 0) {
            if (LOG.isDebugEnabled())
                LOG.debug("Adding new download data item: " + data);
            downloadDataList.add(data);
        }
        
        if(forV == null || url == null || style == null) {
            LOG.error("Missing required for, url, or style.");
            return;
        }
        
        if(fromV == null)
            fromV = "0.0.0";
        if(toV == null)
            toV = forV;

        try {
            data.setFromVersion(new Version(fromV));
            data.setToVersion(new Version(toV));
            data.setForVersion(new Version(forV));
            if(javaFrom != null)
                data.setFromJava(new Version(javaFrom));
            if(javaTo != null)
                data.setToJava(new Version(javaTo));
        } catch(VersionFormatException vfe) {
            LOG.error("Invalid version", vfe);
            return;
        }
        
        if(pro == null && free == null) {
            data.setPro(true);
            data.setFree(true);
        } else {
            data.setPro(pro != null);
            data.setFree(free != null);
        }
        
        // Update the URL to contain the correct pro & language.
        if(url.indexOf('?') == -1)
            url += "?";
        else
            url += "&";
        url += "pro="   + CommonUtils.isPro() + 
               "&lang=" + encode(ApplicationSettings.getLanguage()) +
               "&lv="   + encode(CommonUtils.getLimeWireVersion()) +
               "&jv="   + encode(CommonUtils.getJavaVersion()) +
               "&os="   + encode(CommonUtils.getOS()) +
               "&osv="  + encode(CommonUtils.getOSVersion());
        data.setUpdateURL(url);
        
        try {
            data.setStyle(Integer.parseInt(style));
        } catch(NumberFormatException nfe) {
            LOG.error("Invalid style", nfe);
            return;
        }
        
        if(os == null)
            os = "*";
        data.setOSList(OS.createFromList(os));
                
        NodeList children = msg.getChildNodes();
        for(int i = 0; i < children.getLength(); i++) {
            Node child = children.item(i);
            if("lang".equals(child.getNodeName()))
                parseLangItem((UpdateData)data.clone(), child);
        }
    }
    
    /**
     * Parses a single lang item.
     *
     * Accepts attributes 'id', 'button1', and 'button2'.
     * 'id' is REQUIRD.  others are optional.
     * REQUIRES a text content inside.
     */
    private void parseLangItem(UpdateData data, Node lang) {
        // Parse the id & url & current attributes -- all MUST exist.
        NamedNodeMap attr = lang.getAttributes();
        String id = getAttributeText(attr, "id");
        String button1 = getAttributeText(attr, "button1");
        String button2 = getAttributeText(attr, "button2");
        String title = getAttributeText(attr, "title");
        String msg = LimeXMLUtils.getTextContent(lang);
        
        if(id == null || msg == null || msg.equals("")) {
            LOG.error("Missing id or message.");
            return;
        }
            
        data.setLanguage(id);
        data.setButton1Text(button1);
        data.setButton2Text(button2);
        data.setUpdateText(msg);
        data.setUpdateTitle(title);
        
        // A-Okay -- we've got a good UpdateData.
        updateDataList.add(data);
    }
    
    /**
     * Gets the text from an attribute map.
     */
    private String getAttributeText(NamedNodeMap map, String attr) {
        Node node = map.getNamedItem(attr);
        if(node != null)
            return node.getNodeValue();
        else
            return null;
    }
    
    /**
     * Converts a string into url encoding.
     */
    private String encode(String unencoded) {
        return URLEncoder.encode(unencoded);
    }
}