package be.iminds.ilabt.jfed.util;

import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceUrn;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/*URN format:
*
* The syntax of URNs, in Backus-Naur form, is:[1]
*
*     <URN> ::= "urn:" <NID> ":" <NSS>
*
* This renders as:
*
*     urn:<NID>:<NSS>
*
* The leading urn: sequence is case-insensitive. <NID> is the namespace identifier, which determines the syntactic
* interpretation of <NSS>, the namespace-specific string. The functional requirements for uniform resource names are
* described in RFC 1737.
*/

/*
* public ID urns: http://www.ietf.org/rfc/rfc3151.txt
*
* The persistence of URNs in the "publicid" namespace is the same as
* the persistence of the corresponding public identifier.
*    *  The "publicid" namespace is available for a wide range of uses; it
* cannot be subjected to a uniform persistence policy.  As a general
* rule, formal public identifiers with registered owner identifiers
* are more likely to be persistent than informal public identifiers
* or formal public identifiers with unregistered owner identifiers.
*    *  One exception to this rule is the "IDN" scheme for producing a
* registered owner identifier from a domain name.  That scheme
* contains at least all the weaknesses associated with the
* persistence of domain names.
*/

/*
* urn publicid IDN scheme:
*
* ?
* */

/*
* Geni URN info:
*
* @link http://www.protogeni.net/wiki/URNs
* @link http://groups.geni.net/geni/wiki/GeniApiIdentifiers
*
*  The definitive description of URN structure is given in the GMOC proposal (http://testglobalnoc.grnoc.iu.edu/gmoc/file-bin/urn-proposal2.pdf).
*  However, as an informational
*  summary, ProtoGENI URNs can be considered in the form:
*
*      urn:publicid:IDN+toplevelauthority+resource-type+resource-name
*
*  where:
*
*  toplevelauthority
*      is an internationali[sz]ed domain name (which must match the one in the certificate of the authority which
*      issued the object name)
*  resource-type
*      is a string describing the type of the named object (the set of strings is described below)
*  resource-name
*      should uniquely identify the object among any other resources with identical toplevelauthority and
*      resource-type. It is important to realise that the ProtoGENI API attaches no other significance to this
*      field, and in particular, no relation is implied between objects with identical resource-name but differing
*      toplevelauthority or resource-type. However, individual authorities (and especially component managers) may
*      choose to define additional semantics for resource names.
* */
public class GeniUrn extends ResourceUrn {
    /**
     * <reserved>    ::= '%" | "/" | "?" | "#"
     <excluded> ::= octets 1-32 (1-20 hex) | "\" | """ | "&" | "<"
     | ">" | "[" | "]" | "^" | "`" | "{" | "|" | "}" | "~"
     | octets 127-255 (7F-FF hex)
     */
    private static final Pattern EXCLUDED_URN_PATTERNS = Pattern.compile("[/#?\\\\\"&<>\\[\\]^`{|}~\\x7F-\\xFF\\x00-\\x20]");
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(GeniUrn.class);
    /**
     * urn:publicid:IDN+toplevelauthority[:sub-authority]*\+resource-type\+resource-name
     */
    private static final String URN_REGEX =
            "urn:publicid:IDN\\+([0-9a-zA-Z._:-]*)\\+([a-zA-Z]*)\\+([+;%/0-9a-zA-Z._:-]*)";
    private static final Pattern URN_PATTERN = Pattern.compile(URN_REGEX);

    public static GeniUrn createGeniUrnFromEncodedParts(String topLevelAuthority, String resourceType, String resourceName) {
        return new GeniUrn(topLevelAuthority, resourceType, resourceName, true/*encoded*/);
    }
    public static GeniUrn createGeniUrnFromDecodedParts(String topLevelAuthority, String resourceType, String resourceName) {
        return new GeniUrn(topLevelAuthority, resourceType, resourceName, false/*encoded*/);
    }

    public static GeniUrn createGeniUrnFromPublicID(String publicIdString) {
        // example: IDN plc//princeton authority sa
        String[] parts = publicIdString.split(" ", 4);
        if (parts.length > 1 && !Objects.equals(parts[0], "IDN"))
            throw new IllegalArgumentException("Not a public ID String, should start with \"IDN\": \""+publicIdString+"\"");
        if (parts.length < 4)
            throw new IllegalArgumentException("Not a public ID String: \""+publicIdString+"\" ("+parts.length+" parts)");
        return createGeniUrnFromDecodedParts(parts[1], parts[2], parts[3]);
    }

    public GeniUrn replaceTopLevelAuthority(String newTopLevelAuthority) {
        return createGeniUrnFromDecodedParts(newTopLevelAuthority, resourceType_dec, resourceName_dec);
    }

    //TODO: add support for sub-authority
    public class GeniUrnParseException extends Exception {
        public GeniUrnParseException(String badUrn) {
            super("String \"" + badUrn + "\" is not a valid Geni Urn");
        }
    }

    private final String topLevelAuthority_enc;
    private final String topLevelAuthority_withoutSubAuth_enc;
    private final String resourceType_enc;
    private final String resourceName_enc;

    private final String topLevelAuthority_dec;
    private final String topLevelAuthority_withoutSubAuth_dec;
    private final String resourceType_dec;
    private final String resourceName_dec;

    /**
     * @throws GeniUrnParseException for invalid urns.
     *                               see parse(String urn) to get null result instead,
     */
    public GeniUrn(String urn) throws GeniUrnParseException {
        super(urn);

        assert urn != null;
        assert EXCLUDED_URN_PATTERNS != null;

        if (EXCLUDED_URN_PATTERNS.matcher(urn).find())
            throw new GeniUrnParseException(urn);


        Matcher urnMatcher = URN_PATTERN.matcher(urn);
        if (!urnMatcher.matches())
            throw new GeniUrnParseException(urn);
        this.topLevelAuthority_enc = urnMatcher.group(1);
        this.resourceType_enc = urnMatcher.group(2);
        this.resourceName_enc = urnMatcher.group(3);
        this.topLevelAuthority_dec = urnDecoding(topLevelAuthority_enc);
        this.resourceType_dec = urnDecoding(resourceType_enc);
        this.resourceName_dec = urnDecoding(resourceName_enc);

        if (topLevelAuthority_dec == null || resourceType_dec == null || resourceName_dec == null)
            throw new GeniUrnParseException(urn);

//        if (this.resourceName.contains("+"))
//            throw new GeniUrnParseException(urn);

        if (topLevelAuthority_dec.isEmpty() || resourceType_dec.isEmpty() || resourceName_dec.isEmpty())
            throw new GeniUrnParseException(urn);

        topLevelAuthority_withoutSubAuth_enc = topLevelAuthority_enc.replaceFirst(":.*", "");
        topLevelAuthority_withoutSubAuth_dec = urnDecoding(topLevelAuthority_withoutSubAuth_enc);
    }

    /**
     * returns null for invalid urns.
     */
    @Nullable
    public static GeniUrn parse(String urn) {
        if (urn == null) return null;
        try {
            return new GeniUrn(urn);
        } catch (GeniUrnParseException e) {
            return null;
        }
    }

    /* from https://tools.ietf.org/html/rfc3151

      -  A space in the public identifier is transcribed as
         "+".  Whitespace normalization must be performed
         before constructing a URN in the "publicid" namespace,
         therefore adjacent "+" characters never occur in URNs
         in this namespace.
      -  The sequence of characters "//" is transcribed as ":".
      -  The sequence of characters "::" is transcribed as ";".
      -  A literal "+" character is transcribed as "%2B".
      -  A literal ":" character (except in "::") is
         transcribed as "%3A".
      -  A literal "/" character (except in "//") is
         transcribed as "%2F".
      -  A literal ";" character is transcribed as "%3B".
      -  A literal "'" character is transcribed as "%27".
      -  A literal "?" character is transcribed as "%3F".
      -  A literal "#" character is transcribed as "%23".
      -  A literal "%" character is transcribed as "%25".
    */

    public static String urnEncoding(String text) {
        text = text.trim();
        String res = "";
        boolean prevIsWhitespace = false;
        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);
            char d = i < text.length()-1 ? text.charAt(i+1) : 'X';

            if (Character.isWhitespace(c)) {
                if (!prevIsWhitespace)
                    res += "+";
                prevIsWhitespace = true;
            } else {
                prevIsWhitespace = false;
                switch (c) {
                    case '+': //fall-through
                    case ';': //fall-through
                    case '\''://fall-through
                    case '?': //fall-through
                    case '#': //fall-through
                    case ':': {
                        if (d == ':') {
                            res += ';';
                            i++;
                            break;
                        }
                        //else  //fall-through
                    }
                    case '/': {
                        if (d == '/') {
                            res += ':';
                            i++;
                            break;
                        }
                        //else  //fall-through
                    }
                    //encode to hex ASCII code
                    case '%': { res += "%"+Integer.toHexString((int) c).toUpperCase(); break; }
                    default: {
                        res += c;
                        break;
                    }
                }
            }
        }
        return res;
    }
    public static String urnDecoding(String urnText) {
        String res = "";

        for (int i = 0; i < urnText.length(); i++) {
            char c = urnText.charAt(i);
            String next2 = "";
            if (i < urnText.length() - 2) {
                next2 = urnText.substring(i+1, i+3);
            }
            switch (c) {
                case ':' : {
                    res += "//";
                    break;
                }
                case ';' : {
                    res += "::";
                    break;
                }
                case '+' : {
                    res += " ";
                    break;
                }
                case '%': {
                    try {
                        //decode hex ASCII code
                        int charCode = Integer.parseInt(next2, 16);
                        res += (char) charCode;
                        i += 2;
                    } catch (NumberFormatException e) {
                        res += c;
                    }
                    break;
                }
                default: {
                    res += c;
                    break;
                }
            }
        }

        return res;
    }

    public static GeniUrn removeSubAuth(GeniUrn urn){
        return createGeniUrnFromEncodedParts(urn.getEncodedTopLevelAuthority_withoutSubAuth(), urn.getEncodedResourceType(), urn.getEncodedResourceName());
    }

    /**
     * returns null for invalid urns.
     */
    public static boolean valid(String urn) {
        if (urn == null) return false;
        try {
            new GeniUrn(urn);
            return true;
        } catch (GeniUrnParseException e) {
            return false;
        }
    }

    private GeniUrn(String topLevelAuthority_in, String resourceType_in, String resourceName_in, boolean encoded) {
        super("dummy"); //set below: super.value

        if (encoded) {
            this.topLevelAuthority_enc = topLevelAuthority_in;
            this.resourceType_enc = resourceType_in;
            this.resourceName_enc = resourceName_in;
            this.topLevelAuthority_dec = urnDecoding(topLevelAuthority_enc);
            this.resourceType_dec = urnDecoding(resourceType_enc);
            this.resourceName_dec = urnDecoding(resourceName_enc);
        } else {
            this.topLevelAuthority_enc = urnEncoding(topLevelAuthority_in);
            this.resourceType_enc = urnEncoding(resourceType_in);
            this.resourceName_enc = urnEncoding(resourceName_in);
            this.topLevelAuthority_dec = urnDecoding(topLevelAuthority_enc);
            this.resourceType_dec = urnDecoding(resourceType_enc);
            this.resourceName_dec = urnDecoding(resourceName_enc);
        }

        super.value = toString();

        assert this.topLevelAuthority_dec != null;
        assert this.resourceType_dec != null;
        assert this.resourceName_dec != null;
        assert !this.topLevelAuthority_dec.isEmpty();
        assert !this.resourceType_dec.isEmpty();
        assert !this.resourceName_dec.isEmpty();

        topLevelAuthority_withoutSubAuth_enc = topLevelAuthority_enc.replaceFirst(":.*", "");
        topLevelAuthority_withoutSubAuth_dec = urnDecoding(topLevelAuthority_withoutSubAuth_enc);
    }

    public String getEncodedTopLevelAuthority() {
        return topLevelAuthority_enc;
    }

    public String getEncodedTopLevelAuthority_withoutSubAuth() {
        return topLevelAuthority_withoutSubAuth_enc;
    }

    public String getEncodedResourceType() {
        return resourceType_enc;
    }

    public String getEncodedResourceName() {
        return resourceName_enc;
    }

    public String getTopLevelAuthority() {
        return topLevelAuthority_dec;
    }

    public String getTopLevelAuthority_withoutSubAuth() {
        return topLevelAuthority_withoutSubAuth_dec;
    }

    public String getResourceType() {
        return resourceType_dec;
    }

    public String getResourceName() {
        return resourceName_dec;
    }


    public boolean hasSubAuthority() {
        return topLevelAuthority_enc.contains(":");
    }

    /**
     * @return sub authority name of topLevelAuthority, or null if none. If there are multiple sub authorities, this returns them all, concatenated with a colon
     */
    @Nullable
    public String getEncodedSubAuthName() {
        String[] s = topLevelAuthority_enc.split(":", 2);
        if (s.length < 2) return null;
        return s[1];
    }
    @Nullable
    public String getSubAuthName() {
        String[] s = topLevelAuthority_dec.split("//", 2);
        if (s.length < 2) return null;
        return s[1];
    }

    /**
     * @param limit the number of sub authorities to return. Must be >= 1
     * @return a number of sub authorities (concatenated with a colon), starting with the lowest. Returns null if there are not enough sub authorities.
     */
    @Nullable
    public String getEncodedSubAuthNames(int limit) {
        if (limit < 1) {
            LOG.error("Bad call: limit="+limit+" but must be >= 1");
            return null;
        }
        assert limit >= 1;
        String[] s = topLevelAuthority_enc.split(":");
        if (s.length <= limit) return null;
        String res = s[1];
        for (int i = 2; i <= limit; i++)
            res += ":"+s[i];
        return res;
    }
    @Nullable
    public String getSubAuthNames(int limit) {
        if (limit < 1) {
            LOG.error("Bad call: limit="+limit+" but must be >= 1");
            return null;
        }
        assert limit >= 1;
        String[] s = topLevelAuthority_dec.split("//");
        if (s.length <= limit) return null;
        String res = s[1];
        for (int i = 2; i <= limit; i++)
            res += "//"+s[i];
        return res;
    }

    /**
     *
     * @param index the index of the sub authority name. (the first sub authority is at 0)
     * @return the specified sub authority name, or null if there is none
     */
    @Nullable
    public String getEncodedSubAuthName(int index) {
        String[] s = topLevelAuthority_enc.split(":");
        if (s.length <= index+1) return null;
        return s[index+1];
    }
    @Nullable
    public String getSubAuthName(int index) {
        String[] s = topLevelAuthority_dec.split("//");
        if (s.length <= index+1) return null;
        return s[index+1];
    }

    public int getSubAuthCount() {
        String[] s = topLevelAuthority_enc.split(":");
        if (s.length < 2) return 0;
        return s.length - 1;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;

        //too bad I can't do the same in String.equals
        if (o instanceof String)
            LOG.warn("Comparing String to GeniUrn, this is most likely a bug! (will always be false)", new RuntimeException("bug"));

        if (o == null || getClass() != o.getClass()) return false;

        GeniUrn geniUrn = (GeniUrn) o;

        if (!Objects.equals(resourceName_enc, geniUrn.resourceName_enc)) return false;
        if (!Objects.equals(resourceType_enc, geniUrn.resourceType_enc)) return false;
        if (!Objects.equals(topLevelAuthority_enc, geniUrn.topLevelAuthority_enc)) return false;

        return true;
    }

    /**
     * are the URNs equal, ignoring the sub authority part?
     */
    public boolean equalsIgnoringSubAuth(Object o) {
        if (this == o) return true;

        //too bad I can't do the same in String.equals
        if (o instanceof String)
            LOG.warn("Comparing String to GeniUrn, this is most likely a bug! (will always be false)", new RuntimeException("bug"));

        if (o == null || getClass() != o.getClass()) return false;

        GeniUrn geniUrn = (GeniUrn) o;

        if (!Objects.equals(resourceName_enc, geniUrn.resourceName_enc)) return false;
        if (!Objects.equals(resourceType_enc, geniUrn.resourceType_enc)) return false;
        if (!Objects.equals(topLevelAuthority_withoutSubAuth_enc, geniUrn.topLevelAuthority_withoutSubAuth_enc)) return false;

        return true;
    }

    /**
     * returns true only if:
     *   - the top level authority matches
     *   - all subauthorities "on the same level" match
     * This ignores sub auhorities that the other URN does no have.
     * 
     * Note that a.hasCompatibleAuthority(b) always matches b.hasCompatibleAuthority(a)
     * 
     * So TLD:A:B:C matches TLD, TLD:A, TLD:A:B, TLD:A:B:C and TLD:A:B:C:Z (and all of the reverse)
     * 
     * So TLD:A:B:C does not match TLD:X, TLD:X:Y:Z, TLD:A:Y, TLD:A:B:Z (and the reverse)
     * So also, TLD:A:X does not match TLD:A:Y or TLD:A:Y:Z
     * 
    * */
    public boolean hasCompatibleAuthority(GeniUrn o) {
        if (!Objects.equals(getEncodedTopLevelAuthority_withoutSubAuth(), o.getEncodedTopLevelAuthority_withoutSubAuth()))
            return false;
        //top level authority matches

        if (hasSubAuthority() && o.hasSubAuthority()) {
            int minSubAuthCount = Math.min(getSubAuthCount(), o.getSubAuthCount());
            assert minSubAuthCount > 0;
            String sliverSubAuth = getEncodedSubAuthNames(minSubAuthCount);
            String compManSubAuth = o.getEncodedSubAuthNames(minSubAuthCount);
            if (!Objects.equals(sliverSubAuth, compManSubAuth))
                return false; // -> sub authorities differ
            //any "overlapping" sub authorities match
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = topLevelAuthority_enc.hashCode();
        result = 31 * result + resourceType_enc.hashCode();
        result = 31 * result + resourceName_enc.hashCode();
        return result;
    }

    public String toString() {
        return "urn:publicid:IDN+" + topLevelAuthority_enc + "+" + resourceType_enc + "+" + resourceName_enc;
    }


    public static class JsonDeserializer extends com.fasterxml.jackson.databind.JsonDeserializer<GeniUrn> {
        @Override
        public GeniUrn deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
            JsonNode node = jsonParser.getCodec().readTree(jsonParser);

            if (node.isTextual()) {
                String urnStr = node.asText();
                try {
                    return new GeniUrn(urnStr);
                } catch (GeniUrnParseException e) {
                    throw new IOException("Failed to parse GeniUrn \""+urnStr+"\"", e);
                }
            } else
                throw new IOException("Invalid JSON type for GeniUrn. Must be JSON string, but is "+node.getNodeType().name());
        }
    }

    public static class JsonSerializer extends com.fasterxml.jackson.databind.JsonSerializer<GeniUrn> {
        @Override
        public void serialize(GeniUrn geniUrn, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            if (geniUrn == null)
                return;

            jsonGenerator.writeString(geniUrn.getValue());
        }
    }


//    //ResourceId implementation
//    public String getType() { return "urn"; }
//    public String getValue() { return toString(); }
}
