/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.html.parser;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netbeans.modules.html.editor.lib.api.HelpResolver;
import org.openide.filesystems.FileUtil;
import org.openide.modules.InstalledFileLocator;
import org.openide.util.CharSequences;

/**
 *
 * @author marekfukala
 */
public class HtmlDocumentation implements HelpResolver {

    static final String SECTIONS_PATTERN_CODE = "<h\\d\\s*?id=['\\\"]?([\\w\\d-_,:]*)['\\\"]?[^\\>]*>";//NOI18N
//    static final String SECTIONS_PATTERN_CODE ="<[\\w\\d]*.*?id=\\\"([\\w\\d-_]*)\\\"[^\\>]*>";//NOI18N
    static final Pattern SECTIONS_PATTERN = Pattern.compile(SECTIONS_PATTERN_CODE);
    private static final String DOC_ZIP_FILE_NAME = "docs/html5doc.zip"; //NOI18N
    private static URL DOC_ZIP_URL;
    private static final String HELP_PREFIX = "<html><head><title>help</title></head><body>"; //NOI18N
    private static final HtmlDocumentation SINGLETON = new HtmlDocumentation();
    //performance unit testing
    static long url_read_time, pattern_search_time;

    private static Map<String, String> HELP_FILES_CACHE = new WeakHashMap<String, String>();
    private static Map<URL, OffsetRange> HELP_LINKS_CACHE = new WeakHashMap<URL, OffsetRange>();

    public static void setupDocumentationForUnitTests() {
        System.setProperty("netbeans.dirs", System.getProperty("cluster.path.final"));//NOI18N
    }

    public static HtmlDocumentation getDefault() {
        return SINGLETON;
    }

    static URL getZipURL() {
        if (DOC_ZIP_URL == null) {
            File file = InstalledFileLocator.getDefault().locate(DOC_ZIP_FILE_NAME, null, false);
            if (file != null) {
                try {
                    URL url = file.toURI().toURL();
                    DOC_ZIP_URL = FileUtil.getArchiveRoot(url);
                } catch (MalformedURLException ex) {
                    Logger.getLogger(HtmlDocumentation.class.getName()).log(Level.SEVERE, null, ex);
                }
            } else {
                Logger.getAnonymousLogger().warning(String.format("Cannot locate the %s documentation file.", DOC_ZIP_FILE_NAME)); //NOI18N
            }
        }
        return DOC_ZIP_URL;
    }

    public URL resolveLink(URL baseURL, String relativeLink) {
        String link = null;
        String base = baseURL.toExternalForm();

        if (relativeLink.startsWith("#")) {
            //link within the same file
            int hashIdx = base.indexOf('#');
            if (hashIdx != -1) {
                base = base.substring(0, hashIdx);
            }
            link = base + relativeLink;
        } else {
            //link contains a filename
            link = getZipURL() + relativeLink;
        }

        try {
            return new URI(link).toURL();
        } catch (URISyntaxException ex) {
            Logger.getLogger(HtmlDocumentation.class.getName()).log(Level.SEVERE, null, ex);
        } catch (MalformedURLException ex) {
            Logger.getLogger(HtmlDocumentation.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }

    public URL resolveLink(String relativeLink) {
        if (relativeLink == null) {
            return null;
        }
        URL zipURL = getZipURL();
        if(zipURL == null) {
            return null;
        }
        
        try {
            return new URI(zipURL.toExternalForm() + relativeLink).toURL();
        } catch (URISyntaxException ex) {
            Logger.getLogger(HtmlDocumentation.class.getName()).log(Level.SEVERE, null, ex);
        } catch (MalformedURLException ex) {
            Logger.getLogger(HtmlDocumentation.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }

    public String getHelpContent(URL url) {
        return getSectionContent(url, null);
    }

    static String getContentAsString(URL url, Charset charset) {
        String filePath = url.getPath();
        String cachedContent = HELP_FILES_CACHE.get(filePath);
        if(cachedContent != null) {
            return cachedContent;
        }

        if (charset == null) {
            charset = Charset.defaultCharset();
        }
        try {
            URLConnection con = url.openConnection();
            con.connect();
            Reader r = new InputStreamReader(new BufferedInputStream(con.getInputStream()), charset);
            char[] buf = new char[2048];
            int read;
            StringBuilder content = new StringBuilder();
            while ((read = r.read(buf)) != -1) {
                content.append(buf, 0, read);
            }
            r.close();
            String strContent = content.toString();
            HELP_FILES_CACHE.put(filePath, strContent);
            return strContent;
        } catch (IOException ex) {
            Logger.getLogger(HtmlDocumentation.class.getName()).log(Level.SEVERE, null, ex);
        }

        return null;
    }

    public String getSectionContent(URL url, Charset charset) {
        long a = System.currentTimeMillis();
        String content = getContentAsString(url, charset);
        long b = System.currentTimeMillis();
        String surl = url.toExternalForm();
        int hashIndex = surl.indexOf('#');
        if (hashIndex == -1) {
            //no anchor, return whole content
            return content;
        }

        //tro to use cache
        OffsetRange range = HELP_LINKS_CACHE.get(url);
        if(range != null) {
            return buildHelpText(content, range);
        }

        //anchor
        String sectionName = surl.substring(hashIndex + 1);
        Matcher matcher = SECTIONS_PATTERN.matcher(content);

        int from = -1;
        int to = -1;
        List<Integer> groupIndexes = new LinkedList<Integer>();
        while (matcher.find()) {
            groupIndexes.add(matcher.start());

            if (matcher.group(1).equals(sectionName)) {
                from = matcher.start();
            } else if (from != -1) {
                //start of another section
                to = matcher.start();
                break;
            }
        }
        if (to == -1) {
            to = content.length();
        }
        long c = System.currentTimeMillis();
        url_read_time = (b - a);
        pattern_search_time = (c - b);

        if (from != -1) {
            return buildAndCacheHelpText(content, new OffsetRange(from, to), url);
        } else {
            //no heading found for the link, lets look into the heading groups and possibly
            //find a link in the <dfn id="..."/> form
            int lastgi = -1;
            for (int gi : groupIndexes) {
                if (lastgi != -1) {
                    //heading section <lastgi:gi>
                    //lets try to find the link inside
                    CharSequence sub = content.subSequence(lastgi, gi);
                    int index = CharSequences.indexOf(
                            sub,
                            String.format("<dfn id=%s", sectionName)); //NOI18N
                    if (index != -1) {
                        //use this section
                        return buildAndCacheHelpText(content, new OffsetRange(lastgi, gi), url);
                    }
                }
                lastgi = gi;
            }
        }

        return null;
    }

    private String buildAndCacheHelpText(String helpFileContent, OffsetRange strippedArea, URL url) {
        HELP_LINKS_CACHE.put(url, strippedArea);
        return buildHelpText(helpFileContent, strippedArea);
    }

    private String buildHelpText(String helpFileContent, OffsetRange strippedArea) {
        return new StringBuilder().append(HELP_PREFIX).append(helpFileContent.subSequence(strippedArea.from, strippedArea.to)).toString();
    }

    private static class OffsetRange {
        public int from, to;
        public OffsetRange(int from, int to) {
            this.from = from;
            this.to = to;
        }
    }
    
}
