/*
 * Copyright (c) 2007-2010 by The Broad Institute, Inc. and the Massachusetts Institute of Technology.
 * All Rights Reserved.
 *
 * This software is licensed under the terms of the GNU Lesser General Public License (LGPL), Version 2.1 which
 * is available at http://www.opensource.org/licenses/lgpl-2.1.php.
 *
 * THE SOFTWARE IS PROVIDED "AS IS." THE BROAD AND MIT MAKE NO REPRESENTATIONS OR WARRANTIES OF
 * ANY KIND CONCERNING THE SOFTWARE, EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT
 * OR OTHER DEFECTS, WHETHER OR NOT DISCOVERABLE.  IN NO EVENT SHALL THE BROAD OR MIT, OR THEIR
 * RESPECTIVE TRUSTEES, DIRECTORS, OFFICERS, EMPLOYEES, AND AFFILIATES BE LIABLE FOR ANY DAMAGES OF
 * ANY KIND, INCLUDING, WITHOUT LIMITATION, INCIDENTAL OR CONSEQUENTIAL DAMAGES, ECONOMIC
 * DAMAGES OR INJURY TO PROPERTY AND LOST PROFITS, REGARDLESS OF WHETHER THE BROAD OR MIT SHALL
 * BE ADVISED, SHALL HAVE OTHER REASON TO KNOW, OR IN FACT SHALL KNOW OF THE POSSIBILITY OF THE
 * FOREGOING.
 */
package org.broad.igv.das;

import org.apache.log4j.Logger;
import org.broad.igv.data.DataSource;
import org.broad.igv.exceptions.DataLoadException;
import org.broad.igv.feature.*;
import org.broad.igv.track.FeatureSource;
import org.broad.igv.ui.WaitCursorManager;
import org.broad.igv.ui.util.MessageUtils;
import org.broad.igv.util.IGVHttpUtils;
import org.broad.igv.util.LRUCache;
import org.broad.igv.util.ResourceLocator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.traversal.DocumentTraversal;
import org.w3c.dom.traversal.NodeFilter;
import org.w3c.dom.traversal.TreeWalker;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;


//http://das.sanger.ac.uk/das/cosmic_mutations/

public class DASFeatureSource implements FeatureSource {

    private static Logger log = Logger.getLogger(DASFeatureSource.class);
    private LRUCache<String, List<Feature>> featureCache = new LRUCache<String, List<Feature>>(30);
    private String serverPath;
    private String[] capabilities;
    private String label;
    private boolean isValid = true;

    public DASFeatureSource(ResourceLocator locator) {
        validatePath(pathConform(locator.getPath()));
    }

    public Iterator<Feature> getFeatures(String chr, int start, int end) {
        return getFeatures(chrConform(chr)).iterator();
    }

    private String pathConform(String path) {

        if (path.endsWith("/")) {
            return path = path.substring(0, path.length() - 1);
        }

        String[] parts = path.split("/");
        if (parts[parts.length - 1].equalsIgnoreCase("feature")) {
            return path.replace(parts[parts.length - 1], "");
        }

        return path;
    }

    private List<Feature> getFeatures(String chr) {
        chr = chrConform(chr);
        if (!chr.equals("All")) {
            if (!isCapable("feature")) {
                throw new DataLoadException("Server is not Feature Capable", serverPath);
            }
            if (featureCache.containsKey(chr)) {
                return featureCache.get(chr);
            } else {
                if (isValid) {
                    WaitCursorManager.CursorToken token = WaitCursorManager.showWaitCursor();
                    try {
                        List<Feature> features = gatherFeatures(chr, 1, Integer.MAX_VALUE);
                        if (features.size() < 1) {
                            return Collections.emptyList();
                        }
                        FeatureUtils.sortFeatureList(features);
                        featureCache.put(chr, features);
                        return featureCache.get(chr);
                    } finally {
                        WaitCursorManager.removeWaitCursor(token);
                    }
                }
            }
        }
        return new ArrayList<Feature>();
    }

    

    public List<LocusScore> getCoverageScores(String chr, int startLocation, int endLocation, int zoom) {
        return null;  //To change body of implemented methods use File | Settings | File Templates.
    }

    public int getBinSize() {
        return 0;  //To change body of implemented methods use File | Settings | File Templates.
    }

    public void setBinSize(int size) {
        // ignored
    }

    public DataSource getCoverageSource() {
        return null;  //To change body of implemented methods use File | Settings | File Templates.
    }


    private String chrConform(String chr) {
        if (chr.startsWith("chr")) {
            return chr.substring(3, chr.length());
        }
        return chr;
    }

    private List<Feature> gatherFeatures(String chr, int start, int stop) {
        List<Feature> features = new ArrayList<Feature>();
        try {
            URL dataQuery = new URL(serverPath + "/features?" + "segment=" + chr + ":" + start + "," + stop);
            Document dom = getDocument(dataQuery);
            if (dom == null) {
                return Collections.emptyList();
            }
            return parseDocument(dom, chr, features);

        } catch (IOException ioe) {
            throw new DataLoadException("Failed to reconnect with server", serverPath);
        }
    }

    private void validatePath(String path) {

        try {
            URL serverURL = new URL(path);
            HttpURLConnection connection = IGVHttpUtils.openConnection(serverURL);
            String headerCaps = connection.getHeaderField("X-DAS-Capabilities");
            if (headerCaps == null) {
                throw new DataLoadException("No DAS capabilities found, closing connection.", path);
            }

            capabilities = headerCaps.split(";");
            String[] parts = path.split("//");
            if (parts.length > 1)
                path = parts[1];
            if (path.length() > 15) {
                label = path.substring(0, 15) + "..";
            } else {
                label = path;
            }
            serverPath = serverURL.toExternalForm();
            connection.disconnect();
        } catch (IOException ioe) {
            throw new DataLoadException("Could not complete connection to server", path);
        }
    }

    private Document getDocument(URL query) {
        InputStream is = null;
        try {
            HttpURLConnection serverConnection = IGVHttpUtils.openConnection(query);
            serverConnection.connect();
            is = serverConnection.getInputStream();
            return createDocument(is);
        } catch (Exception e) {
            isValid = false;
            log.error(e);
            MessageUtils.showMessage("<html>The DAS Server: " + serverPath + " has returned an error or invalid data." +
                    "<br>" + e.getMessage());
            return null;
        }
        finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
                }
            }
        }
    }

    private Document createDocument(InputStream inputStream)
            throws ParserConfigurationException, IOException, SAXException {
        DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        Document dom = documentBuilder.parse(inputStream);
        inputStream.close();
        return dom;
    }

    public String getPath() {
        return serverPath;
    }

    public boolean isCapable(String capability) {

        if (capabilities != null) {
            for (String a : Arrays.asList(capabilities)) {
                if (a.toLowerCase().contains(capability.toLowerCase())) {
                    return true;
                }
            }
        }
        return false;
    }

    private List<Feature> parseDocument(Document dasDoc, String chr, List<Feature> features) {

        try {
            DocumentTraversal traversal = (DocumentTraversal) dasDoc;
            TreeWalker treewalker = traversal.createTreeWalker(
                    dasDoc.getDocumentElement(), NodeFilter.SHOW_ELEMENT, null, true);
            return parseTree(treewalker, "FEATURE", chr, features);

        } catch (Exception ex) {
            log.error(ex);
        }
        return new ArrayList<Feature>();
    }

    private List<Feature> parseTree(TreeWalker walker,
                                    String tag,
                                    String chr, List<Feature> features) {

        Node parent = walker.getCurrentNode();
        Node n = walker.firstChild();
        while (n != null) {
            if (((Element) n).getTagName().equalsIgnoreCase(tag)) {
                features.add(getFeature(walker, chr));
                n = walker.nextSibling();
//                if (features.size() > 16384){
//                    break;
//                }
                continue;
            }
            parseTree(walker, tag, chr, features);
            n = walker.nextSibling();
        }
        walker.setCurrentNode(parent);
        return features;
    }

    private BasicFeature getFeature(TreeWalker walker, String chr) {

        String start = "0",
                end = "0",
                orientation = "",
                id = "",
                label = "",
                description = "",
                type = "",
                link = "",
                method = "";
        Strand strand = Strand.NONE;
        Node parent = walker.getCurrentNode();

        //GET THE FEATURE ATTRIBUTES
        NamedNodeMap nnm = parent.getAttributes();
        Node tmpNode = nnm.getNamedItem("id");
        id = tmpNode.getTextContent();
        tmpNode = nnm.getNamedItem("label");
        label = tmpNode.getTextContent();

        //GO THROUGH FEATURE NODES
        for (Node n = walker.firstChild(); n != null;
             n = walker.nextSibling()) {

            if (((Element) n).getTagName().equalsIgnoreCase("START")) {
                start = n.getTextContent();
            }
            if (((Element) n).getTagName().equalsIgnoreCase("END")) {
                end = n.getTextContent();
            }
            if (((Element) n).getTagName().equalsIgnoreCase("ORIENTATION")) {
                orientation = n.getTextContent();
                if (orientation.equals("-")) {
                    strand = Strand.NEGATIVE;
                } else if (orientation.equalsIgnoreCase("+")) {
                    strand = Strand.POSITIVE;
                }
            }
            if (((Element) n).getTagName().equalsIgnoreCase("NOTE")) {
                description = n.getTextContent();
            }
            if (((Element) n).getTagName().equalsIgnoreCase("TYPE")) {
                type = n.getTextContent();
            }
            if (((Element) n).getTagName().equalsIgnoreCase("LINK")) {
                NamedNodeMap tmpnnm = n.getAttributes();
                Node tmpnode = tmpnnm.getNamedItem("href");
                link = tmpnode.getTextContent();
            }
            if (((Element) n).getTagName().equalsIgnoreCase("METHOD")) {
                method = n.getTextContent();
            }
        }

        //SET FEATURE ATTRIBUTES

        walker.setCurrentNode(parent);

        // Convert from Ensemble -> UCSC coordinates
        int s = Integer.parseInt(start) - 1;
        int e = Math.max(s + 1, Integer.parseInt(end));

        BasicFeature feature = new BasicFeature(chr, s, e);
        label = label.replace("?", " ");
        description = description.replace("&nbsp;", "<br>");
        description = description.replace("<br><br><br>", "<br>");
        description = description.replace("<br><br>", "<br>");
        description = description.replace(":", ":&nbsp;");
        description = description.replace("?", " ");
        description = description + "<br>TYPE:&nbsp;" + type;
        feature.setIdentifier(id);
        feature.setName(label);
        feature.setDescription(description);
        feature.setType(type);
        if (link.length() > 0) {
            feature.setURL(link);
        }


        //TODO Make another Color panel on the preferences for the different feature colors
        //TODO Would like to base it on one of the kuler panels
        //Using Colors from http://kuler.adobe.com/#themeID/659786

        return feature;
    }
}
