/*
 * Copyright (C) 2010 Teleal GmbH, Switzerland
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.teleal.lemma.anchor;

import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.SeeTag;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.logging.Logger;

/**
 * Encapsulates parsing of citation anchor URIs.
 * <p/>
 * <p>
 * Two addresses are <em>equal</em> if they have the same scheme and path, and (if not null) fragment.
 * </p>
 *
 * @author Christian Bauer
 */
public class AnchorAddress {

    final private static Logger log = Logger.getLogger(AnchorAddress.class.getName());

    protected static final String PATTERN_SCHEME =  "([a-zA-Z]+?)";

    protected static final String PATTERN_PATH = "([ \\p{Alnum}\\./_-]+?)";

    protected static final String PATTERN_FRAGMENT = "([\\p{Alnum}]+?(?:\\([\\p{Alnum},\\.\\[\\]<>\\s]*?\\))??)";

    public static final Pattern PATTERN =
            Pattern.compile("^"+PATTERN_SCHEME+Scheme.SEPARATOR+PATTERN_PATH+"(?:#"+PATTERN_FRAGMENT+")??$");

    public static final String PATH_THIS = "this";

    final private Scheme scheme;
    final private String path;
    final private String fragment;

    public AnchorAddress(Scheme scheme, String path, String fragment) {
        if (path == null || path.length() == 0) {
            throw new IllegalArgumentException("Reference path can not be empty");
        }
        this.scheme = scheme;
        this.path = path;
        if (fragment != null && !fragment.endsWith(")")) fragment = fragment + "()"; // normalize
        this.fragment = fragment;
    }

    public Scheme getScheme() {
        return scheme;
    }

    public String getPath() {
        return path;
    }

    public String getFragment() {
        return fragment;
    }

    public static AnchorAddress valueOf(String string) {
        if (string == null || string.length() == 0) return null;

        Matcher m = PATTERN.matcher(string.trim());
        if (!m.matches()) {
            // If it doesn't then convert it to file:// address and try again
            String fileString = toFileAddress(string);
            log.finest("Address string does not contain a schema, using file schema: " + fileString);
            m.reset(fileString);
            if (!m.matches()) {
                throw new IllegalArgumentException("Invalid reference, no pattern match of anchor: " + string);
            }
        }
        return new AnchorAddress(Scheme.valueOf(m.group(1).toUpperCase()), m.group(2), m.group(3));
    }

    public static AnchorAddress valueOf(Scheme scheme, SeeTag tag) {
        String reference = null;
        if (tag.referencedMember() != null && tag.referencedMember().isMethod()) {
            reference = tag.referencedClassName() + "#" +
                    tag.referencedMember().name() +
                    ((MethodDoc) tag.referencedMember()).flatSignature();
        } else if (tag.referencedClass() != null) {
            reference = tag.referencedClassName();
        } else if (tag.referencedPackage() != null) {
            reference = tag.referencedPackage().name();
        }

        return reference != null
                ? valueOf(scheme.name().toLowerCase() + Scheme.SEPARATOR + reference)
                : null;
    }

    public static AnchorAddress valueOf(Scheme scheme, PackageDoc packageDoc) {
        return new AnchorAddress(
                scheme,
                packageDoc.name(),
                null
        );
    }

    public static AnchorAddress valueOf(Scheme scheme, ClassDoc classDoc, String fragment) {
        return new AnchorAddress(
                scheme,
                classDoc.qualifiedTypeName(),
                fragment
        );
    }

    public static AnchorAddress valueOf(Scheme scheme, MethodDoc methodDoc) {
        return new AnchorAddress(
                scheme,
                methodDoc.containingClass().qualifiedTypeName(),
                methodDoc.name() + methodDoc.flatSignature()
        );
    }

    /**
     * Converts a simple file path into a <code>file://</code> URI string.
     *
     * @param string A file path, e.g. "/my/file.txt".
     * @return A URI string, e.g. "file://my/file.txt";
     */
    public static String toFileAddress(String string) {
        if (string.startsWith(".")) throw new IllegalArgumentException("Reference can't start with a period '.'");

        // Always remove leading and trailing slashes
        if (string.startsWith("/")) string = string.substring(1);
        if (string.endsWith("/")) string = string.substring(0, string.length() - 1);

        return Scheme.FILE + Scheme.SEPARATOR + string;
    }

    /**
     * Generates an XSD:id typed String that can be used in XML documents as identifier attribute value.
     *
     * @return An XSD:id compatible string representation of this URI.
     */
    public String toIdentifierString() {

        String schemeString = getScheme().name().toLowerCase();
        String pathString = getPath();
        String fragmentString = (getFragment() != null ? "#" + getFragment() : "");

        return (schemeString + "." + pathString + fragmentString)
                .replaceAll("\\s", "")
                .replaceAll("#|,", ".")
                .replaceAll("\\[|\\]", "-")
                .replaceAll("\\(|\\)", ".")
                .replaceAll("[^a-zA-Z0-9-._]", "_")
                .replaceAll("__", "_");
    }

/*
    public boolean isMatching(SeeTag tag) {
        // Is the referenced class/member/package of this tag the same as the target of this citation?
        if (tag.referencedPackage() != null) {
            if (!getPath().equals(tag.referencedPackage().name()) || getFragment() != null) {
                return false;
            }
        }
        if (tag.referencedClass() != null) {
            if (!getPath().equals(tag.referencedClassName())) {
                return false;
            }
        }
        if (tag.referencedMember() != null) {
            if (getFragment() == null || !getFragment().equals(tag.referencedMemberName())) {
                return false;
            }
        }
        return true;
    }
*/

    /**
     * @return The complement of the <code>valueOf(String)</code> operation.
     */
    @Override
    public String toString() {
        return getScheme().name().toLowerCase() + Scheme.SEPARATOR + getPath() + (getFragment() != null ? "#" + getFragment() : "");
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        AnchorAddress that = (AnchorAddress) o;

        if (fragment != null ? !fragment.equals(that.fragment) : that.fragment != null) return false;
        if (!path.equals(that.path)) return false;
        if (scheme != that.scheme) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = scheme.hashCode();
        result = 31 * result + path.hashCode();
        result = 31 * result + (fragment != null ? fragment.hashCode() : 0);
        return result;
    }
}
