package org.openstreetmap.gui.jmapviewer.tilesources;

import java.awt.Image;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.regex.Pattern;

import javax.imageio.ImageIO;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.openstreetmap.gui.jmapviewer.Coordinate;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class BingAerialTileSource extends AbstractTSMTileSource {
    private static String API_KEY = "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU";
    private static Future<List<Attribution>> attributions;
    private static String imageUrlTemplate;
    private static Integer imageryZoomMax;
    private static String[] subdomains;

    private static final Pattern subdomainPattern = Pattern.compile("\\{subdomain\\}");
    private static final Pattern quadkeyPattern = Pattern.compile("\\{quadkey\\}");
    private static final Pattern culturePattern = Pattern.compile("\\{culture\\}");

    public BingAerialTileSource() {
        super("Bing Aerial Maps", "http://example.com/");

        if (attributions == null) {
            attributions = Executors.newSingleThreadExecutor().submit(new Callable<List<Attribution>>() {
                public List<Attribution> call() throws Exception {
                    List<Attribution> attrs =  new ArrayList<Attribution>();
                    int waitTime = 1;
                    do {

                        try {
                            attrs = loadAttributionText();
                            System.out.println("Successfully loaded Bing attribution data.");
                            return attrs;
                        }  catch (TileLoadException e) {
                        	System.err.println(e.getMessage());
                        	return attrs;
                        } catch(IOException e) {
                            System.err.println("Could not connect to Bing API. Will retry in " + waitTime + " seconds.");
                            Thread.sleep(waitTime * 1000L);
                            waitTime *= 2;
                        }

                    } while(true);
                }
            });
        }
    }

    class Attribution {
        String attribution;
        int minZoom;
        int maxZoom;
        Coordinate min;
        Coordinate max;
    }

    @Override
    public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
        int t = (zoom + tilex + tiley) % subdomains.length;
        String subdomain = subdomains[t];

        String url = new String(imageUrlTemplate);
        url = subdomainPattern.matcher(url).replaceAll(subdomain);
        url = quadkeyPattern.matcher(url).replaceAll(computeQuadTree(zoom, tilex, tiley));

        return url;
    }

    /*private List<Attribution> loadAttributionText() throws IOException {
        try {
            URL u = new URL("http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?include=ImageryProviders&output=xml&key="
                    + API_KEY);
            URLConnection conn = u.openConnection();

            InputStream stream = conn.getInputStream();

            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(stream);

            XPathFactory xPathFactory = XPathFactory.newInstance();
            XPath xpath = xPathFactory.newXPath();
            imageUrlTemplate = xpath.compile("//ImageryMetadata/ImageUrl/text()").evaluate(document);
            imageUrlTemplate = culturePattern.matcher(imageUrlTemplate).replaceAll(Locale.getDefault().toString());
            imageryZoomMax = Integer.parseInt(xpath.compile("//ImageryMetadata/ZoomMax/text()").evaluate(document));

            NodeList subdomainTxt = (NodeList) xpath.compile("//ImageryMetadata/ImageUrlSubdomains/string/text()").evaluate(document, XPathConstants.NODESET);
            subdomains = new String[subdomainTxt.getLength()];
            for(int i = 0; i < subdomainTxt.getLength(); i++) {
                subdomains[i] = subdomainTxt.item(i).getNodeValue();
            }

            XPathExpression attributionXpath = xpath.compile("Attribution/text()");
            XPathExpression coverageAreaXpath = xpath.compile("CoverageArea");
            XPathExpression zoomMinXpath = xpath.compile("ZoomMin/text()");
            XPathExpression zoomMaxXpath = xpath.compile("ZoomMax/text()");
            XPathExpression southLatXpath = xpath.compile("BoundingBox/SouthLatitude/text()");
            XPathExpression westLonXpath = xpath.compile("BoundingBox/WestLongitude/text()");
            XPathExpression northLatXpath = xpath.compile("BoundingBox/NorthLatitude/text()");
            XPathExpression eastLonXpath = xpath.compile("BoundingBox/EastLongitude/text()");

            NodeList imageryProviderNodes = (NodeList) xpath.compile("//ImageryMetadata/ImageryProvider").evaluate(document, XPathConstants.NODESET);
            List<Attribution> attributions = new ArrayList<Attribution>(imageryProviderNodes.getLength());
            for (int i = 0; i < imageryProviderNodes.getLength(); i++) {
                Node providerNode = imageryProviderNodes.item(i);

                String attribution = attributionXpath.evaluate(providerNode);

                NodeList coverageAreaNodes = (NodeList) coverageAreaXpath.evaluate(providerNode, XPathConstants.NODESET);
                for(int j = 0; j < coverageAreaNodes.getLength(); j++) {
                    Node areaNode = coverageAreaNodes.item(j);
                    Attribution attr = new Attribution();
                    attr.attribution = attribution;

                    attr.maxZoom = Integer.parseInt(zoomMaxXpath.evaluate(areaNode));
                    attr.minZoom = Integer.parseInt(zoomMinXpath.evaluate(areaNode));

                    Double southLat = Double.parseDouble(southLatXpath.evaluate(areaNode));
                    Double northLat = Double.parseDouble(northLatXpath.evaluate(areaNode));
                    Double westLon = Double.parseDouble(westLonXpath.evaluate(areaNode));
                    Double eastLon = Double.parseDouble(eastLonXpath.evaluate(areaNode));
                    attr.min = new Coordinate(southLat, westLon);
                    attr.max = new Coordinate(northLat, eastLon);

                    attributions.add(attr);
                }
            }

            return attributions;
        } catch (SAXException e) {
            System.err.println("Could not parse Bing aerials attribution metadata.");
            e.printStackTrace();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (XPathExpressionException e) {
            e.printStackTrace();
        } catch (Exception e) {
        	e.printStackTrace();
        }
        
        return null;
    }*/
    
    private List<Attribution> loadAttributionText() throws TileLoadException, IOException {
    	List<Attribution> foundAttribs = new ArrayList<Attribution>();
    	
    	try {
            URL u = new URL("http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?include=ImageryProviders&output=xml&key="
                    + API_KEY);
            URLConnection conn = u.openConnection();

            InputStream stream = conn.getInputStream();

            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(stream);
            
            Element eImagery = XMLTileParseUtil.getElement(document, "ImageryMetadata");

            Node nImageUrl=XMLTileParseUtil.getElement(document,eImagery, "ImageUrl");
            if (nImageUrl!=null) {
	            imageUrlTemplate=XMLTileParseUtil.getNodeStrValue(nImageUrl);
	            imageUrlTemplate = culturePattern.matcher(imageUrlTemplate).
	            	replaceAll(Locale.getDefault().toString());
            }

            Node nZoom=XMLTileParseUtil.getElement(document,eImagery,"ZoomMax");
            if (nZoom!=null)
            	imageryZoomMax=XMLTileParseUtil.getNodeIntValue(nZoom);

            Element eSub=XMLTileParseUtil.getElement(document,eImagery,"ImageUrlSubdomains");
            if (eSub !=null){
            	NodeList subdomainTxt = eSub.getElementsByTagName("string");
            	subdomains = new String[subdomainTxt.getLength()];

            	for(int i = 0; i < subdomainTxt.getLength(); i++)
            		subdomains[i] = XMLTileParseUtil.getNodeStrValue(subdomainTxt.item(i));
            }

            NodeList imageryProviderNodes = eImagery.getElementsByTagName("ImageryProvider");
            foundAttribs = new ArrayList<Attribution>(imageryProviderNodes.getLength());

            for (int i = 0; i < imageryProviderNodes.getLength(); i++) {
            	if (imageryProviderNodes.item(i).getNodeType() == Node.ELEMENT_NODE){
            		String attribution = "";
            		Element eProviderNode=(Element)imageryProviderNodes.item(i);

            		Element eAttrib=XMLTileParseUtil.getElement(document,eProviderNode,"Attribution");
            		if (eAttrib!=null)
            			attribution=XMLTileParseUtil.getNodeStrValue(eAttrib);

            		NodeList coverageAreaNodes = eProviderNode.getElementsByTagName("CoverageArea");

            		for(int j = 0; j < coverageAreaNodes.getLength(); j++) {
            			if (coverageAreaNodes.item(j).getNodeType() == Node.ELEMENT_NODE){
            				Element eAreaNode = (Element)coverageAreaNodes.item(j);
            				Attribution attr = new Attribution();
            				attr.attribution = attribution;

            				Node maxZoom=XMLTileParseUtil.getElement(document,eAreaNode,"ZoomMax");
            				if (maxZoom!=null)
            					attr.maxZoom = XMLTileParseUtil.getNodeIntValue(maxZoom);

            				Node minZoom=XMLTileParseUtil.getElement(document,eAreaNode,"ZoomMin");
            				if (minZoom!=null)
            					attr.minZoom = XMLTileParseUtil.getNodeIntValue(minZoom);

            				Node sLat=XMLTileParseUtil.getElement(document,eAreaNode,"SouthLatitude");
            				Double dSouthLat = XMLTileParseUtil.getNodeDblValue(sLat);

            				Node wLong=XMLTileParseUtil.getElement(document,eAreaNode,"WestLongitude");
            				Double dWLong = XMLTileParseUtil.getNodeDblValue(wLong);

            				Node northLat=XMLTileParseUtil.getElement(document,eAreaNode,"NorthLatitude");
            				Double dNorthLat = XMLTileParseUtil.getNodeDblValue(northLat);

            				Node eastLon=XMLTileParseUtil.getElement(document,eAreaNode,"EastLongitude");
            				Double dEastLon = XMLTileParseUtil.getNodeDblValue(eastLon);

            				if ((dSouthLat==null)||(dWLong==null) || (dNorthLat==null) || (dEastLon==null)) {
            					TileLoadException e=new TileLoadException(
            							"Unable to find proper tile " +
            							"coordinates from source. Source " +
            							"meta-data may have been modified.");
            					
            					throw e;
            				}
            				
            				attr.min = new Coordinate(dSouthLat, dWLong);
            				attr.max = new Coordinate(dNorthLat, dEastLon);

            				foundAttribs.add(attr);
            			}
            		}
            	}
            }
        } catch (SAXException e) {
            throw new TileLoadException("Could not parse Bing aerials attribution metadata.");
        } catch (ParserConfigurationException e) {
        	throw new TileLoadException("Could not parse Bing aerials attribution metadata.");
        }
        
        return foundAttribs;
    }

    @Override
    public int getMaxZoom() {
        if(imageryZoomMax != null)
            return imageryZoomMax;
        else
            return 22;
    }

    public TileUpdate getTileUpdate() {
        return TileUpdate.IfNoneMatch;
    }

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

    @Override
    public Image getAttributionImage() {
        try {
            return ImageIO.read(getClass().getResourceAsStream("/org/openstreetmap/gui/jmapviewer/images/bing_maps.png"));
        } catch (IOException e) {
            return null;
        }
    }

    @Override
    public String getAttributionLinkURL() {
        //return "http://bing.com/maps"
        // FIXME: I've set attributionLinkURL temporarily to ToU URL to comply with bing ToU
        // (the requirement is that we have such a link at the bottom of the window)
        return "http://go.microsoft.com/?linkid=9710837";
    }

    @Override
    public String getTermsOfUseURL() {
        return "http://opengeodata.org/microsoft-imagery-details";
    }

    @Override
    public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
        try {
            if (!attributions.isDone())
                return "Loading Bing attribution data...";
            if (attributions.get() == null)
                return "Error loading Bing attribution data";
            StringBuilder a = new StringBuilder();
            for (Attribution attr : attributions.get()) {
                if (zoom <= attr.maxZoom && zoom >= attr.minZoom) {
                    if (topLeft.getLon() < attr.max.getLon() && botRight.getLon() > attr.min.getLon()
                            && topLeft.getLat() > attr.min.getLat() && botRight.getLat() < attr.max.getLat()) {
                        a.append(attr.attribution);
                        a.append(" ");
                    }
                }
            }
            return a.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "Error loading Bing attribution data";
    }

    static String computeQuadTree(int zoom, int tilex, int tiley) {
        StringBuilder k = new StringBuilder();
        for (int i = zoom; i > 0; i--) {
            char digit = 48;
            int mask = 1 << (i - 1);
            if ((tilex & mask) != 0) {
                digit += 1;
            }
            if ((tiley & mask) != 0) {
                digit += 2;
            }
            k.append(digit);
        }
        return k.toString();
    }
}
