//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   Licensed 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 de.ugoe.cs.autoquest.htmlmonitor;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.codec.binary.Base64;

/**
 * <p>
 * This class is used to calculate unique ids for GUI elements and to reuse GUI element objects.
 * For this it provides appropriate create methods. If there is already a GUI element for the
 * given parameters, it is reused. Otherwise, a new one is instantiated with a unique id and
 * returned.
 * </p>
 * 
 * @author Patrick Harms
 */
class HtmlGUIElementManager {

    /**
     * an internal map for GUI elements and their respective ids.
     */
    private Map<String, HtmlGUIElement> idMap = new HashMap<String, HtmlGUIElement>();
    
    /** */
    private Map<String, List<HtmlPageElement>> domPathMap =
          new HashMap<String, List<HtmlPageElement>>();
    
    /**
     * <p>
     * creates a new or reuses an existing GUI element representing a server
     * </p>
     * 
     * @param name the name of the server to represent (must not be null)
     * @param port the port on the server via which the communication is done
     *
     * @return a new or reuses an existing GUI element representing a server
     */
    HtmlServer createHtmlServer(String name, int port) {
        if (name == null) {
            throw new IllegalArgumentException("name must not be null");
        }
        
        String id = calculateId(name, Integer.toString(port));
        
        HtmlGUIElement server = idMap.get(id);
        
        if (server == null) {
            server = new HtmlServer(id, name, port);
            idMap.put(id, server);
        }
        else if (!(server instanceof HtmlServer)) {
            throw new RuntimeException
                ("id conflict: calculated the same id for two different GUI elements");
        }
        
        return (HtmlServer) server;
    }
    
    /**
     * <p>
     * creates a new or reuses an existing GUI element representing a document (web page)
     * </p>
     * 
     * @param server the server on which the document resides (must not be null)
     * @param path   the path of the document on the web server (every following the server spec
     *               in the URL, must not be null)
     * @param query  the query part of the URL pointing to the document
     * @param title  the title of the document if available
     *
     * @return a new or reuses an existing GUI element representing a document
     */
    HtmlDocument createHtmlDocument(HtmlServer server, String path, String query, String title) {
        if (server == null) {
            throw new IllegalArgumentException("server must not be null");
        }
        if (path == null) {
            throw new IllegalArgumentException("path must not be null");
        }

        String id = calculateId(server.getId(), path, query, title);
        
        HtmlGUIElement document = idMap.get(id);

        if (document == null) {
            document = new HtmlDocument(id, server, path, query, title);
            idMap.put(id, document);
        }
        else if (!(document instanceof HtmlDocument)) {
            throw new RuntimeException
                ("id conflict: calculated the same id for two different GUI elements");
        }
        
        return (HtmlDocument) document;
    }

    /**
     * <p>
     * creates a new or reuses an existing GUI element representing an HTML tag in a document
     * </p>
     * 
     * @param document the document to which the HTML tag belongs (must not be null)
     * @param parent   the parent tag, if any
     * @param tagName  the name of the HTML tag (must not be null)
     * @param htmlId   the document wide unique id of the tag, if available
     * @param index    if no HTML id is present, the index of the tag regarding all children in
     *                 the same parent having the same tag name. 
     *
     * @return a new or reuses an existing GUI element representing an HTML tag
     */
    HtmlPageElement createHtmlPageElement(HtmlDocument    document,
                                          HtmlPageElement parent,
                                          String          tagName,
                                          String          htmlId,
                                          Integer         index)
    {
        if (document == null) {
            throw new IllegalArgumentException("document must not be null");
        }
        if (tagName == null) {
            throw new IllegalArgumentException("document must not be null");
        }

        String id = calculateId
            (document.getId(), parent != null ? parent.getDOMPath() : null, tagName, htmlId,
             index != null ? index.toString() : "-1");

        HtmlGUIElement pageElement = idMap.get(id);

        if (pageElement == null) {
            pageElement = new HtmlPageElement(id, document, parent, tagName, htmlId, index);
            idMap.put(id, pageElement);
        }
        else if (!(pageElement instanceof HtmlPageElement)) {
            throw new RuntimeException
                ("id conflict: calculated the same id for two different GUI elements");
        }
            
        List<HtmlPageElement> candidates =
            domPathMap.get(((HtmlPageElement) pageElement).getDOMPath());
        
        if (candidates == null) {
            candidates = new LinkedList<HtmlPageElement>();
            domPathMap.put(((HtmlPageElement) pageElement).getDOMPath(), candidates);
        }
        
        if (!candidates.contains(pageElement)) {
            candidates.add((HtmlPageElement) pageElement);
        }

        return (HtmlPageElement) pageElement;
    }

    /**
     * <p>
     * determines the HTML tag belonging to the given document and residing in the document at the
     * location denoted by the path. The path must be equal to the return value of the
     * {@link HtmlPageElement#getDOMPath()} method.
     * </p>
     *
     * @param document the document to which the searched tag belongs
     * @param domPath  the path through the DOM where the searched tag resists
     * 
     * @return the appropriate HTML tag or null, if none is known
     */
    HtmlPageElement getPageElement(HtmlDocument document, String domPath) {
        List<HtmlPageElement> candidates = domPathMap.get(domPath);
        
        if (candidates != null) {
            for (HtmlPageElement candidate : candidates) {
                if (document.equals(candidate.getDocument())) {
                    return candidate;
                }
            }
        }
        
        return null;
    }

    /**
     * <p>
     * calculates a unique id for the given string fragments using SHA-512 and Base64 encoding.
     * </p>
     *
     * @param fragments strings to be used for calculating a unique id
     * 
     * @return a Base64 encoded unique id for the provided fragments
     */
    private String calculateId(String... fragments) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-512");
            
            for (String fragment : fragments) {
                if (fragment != null) {
                    md.update(fragment.getBytes("UTF-8"));
                }
            }
            
            return Base64.encodeBase64String(md.digest());
        }
        catch (UnsupportedEncodingException e) {
            throw new IllegalStateException("Java VM does not support this code");
        }
        catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("Java VM does not support this code");
        }
    }

}
