package org.jivesoftware.smackx.pubsub;

import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.jxmpp.util.XmppStringUtils;

import java.net.URI;
import java.util.logging.Level;
import java.util.logging.Logger;

public class PubSubUri {

    private static final Logger LOGGER = Logger.getLogger(PubSubUri.class.getName());

    public static final String SCHEME = "xmpp";
    public static final String NODE = ";node=";
    public static final String ITEM = ";item=";
    public static final String ACTION = ";action=";
    public static final String PUBSUB = "pubsub";

    private final BareJid service;
    private final String node;
    private final String item;
    private final Action action;

    public PubSubUri(BareJid service, String node, String item, Action action) {
        this.service = service;
        this.node = node;
        this.item = item;
        this.action = action;
    }

    public PubSubUri(URI uri) throws XmppStringprepException {
        throwIfNoXmppUri(uri);
        String ssp = uri.getSchemeSpecificPart();
        service = getService(ssp);
        node = getNodeOrNull(ssp);
        item = getItemOrNull(ssp);
        action = getActionOrNull(ssp);
    }

    /**
     * Parse a {@link PubSubUri}.
     * This method ignores unknown parameters.
     *
     * @param s string representation of the URI.
     * @return parsed {@link PubSubUri}
     * @throws XmppStringprepException if the string does not represent a valid XMPP PubSub URI.
     */
    public static PubSubUri from(String s)
            throws XmppStringprepException {
        URI uri = URI.create(s);
        return new PubSubUri(uri);
    }

    /**
     * Return the {@link BareJid} of the referenced pubsub service.
     * That may be a {@link org.jxmpp.jid.DomainBareJid} like 'pubsub.shakespeare.lit' or a
     * {@link EntityBareJid} like 'hamlet@denmark.lit'.
     *
     * @return pubsub service jid
     */
    public BareJid getService() {
        return service;
    }

    /**
     * Return the pubsub node name or null if not present.
     *
     * @return node name or null
     */
    public String getNode() {
        return node;
    }

    /**
     * Return the pubsub item name or null if not present.
     *
     * @return item or null
     */
    public String getItem() {
        return item;
    }

    /**
     * Return the {@link Action} of the {@link PubSubUri} or null if not present.
     *
     * @return action or null
     */
    public Action getAction() {
        return action;
    }

    /**
     * Throw an {@link IllegalArgumentException} if the {@link URI URIs} scheme doesn't equal 'xmpp'.
     *
     * @param uri uri
     */
    private static void throwIfNoXmppUri(URI uri) throws XmppStringprepException {
        if (!uri.getScheme().equals(SCHEME)) {
            throw new XmppStringprepException(uri.toString(), "URI is not of scheme 'xmpp'.");
        }
    }

    /**
     * Extract the {@link BareJid} of the pubsub service the URI is pointing to.
     *
     * @param ssp scheme specific part of the URI
     * @return pubsub service jid
     *
     * @throws XmppStringprepException if the service jid is malformed.
     */
    private static BareJid getService(String ssp) throws XmppStringprepException {
        String serviceString = ssp.substring(0, ssp.indexOf("?"));
        // TODO: Use JidCreate.bareFrom(serviceString) once jxmpp is bumped to > 1.0.0
        //  see https://github.com/igniterealtime/jxmpp/pull/22
        throwIfFullJid(serviceString);
        BareJid jid;
        try {
            jid = JidCreate.entityBareFromUrlEncoded(serviceString);
        } catch (XmppStringprepException e) {
            jid = JidCreate.domainBareFromUrlEncoded(serviceString);
        }
        return jid;
    }

    /**
     * Throw an {@link XmppStringprepException} if the provided {@link String} represents a {@link org.jxmpp.jid.FullJid}.
     *
     * @param jid jid as string
     * @throws XmppStringprepException if jid has resource part
     */
    private static void throwIfFullJid(String jid) throws XmppStringprepException {
        if (XmppStringUtils.isFullJID(jid)) {
            throw new XmppStringprepException(jid, "FullJid not allowed here.");
        }
    }

    /**
     * Return the value of the 'node' key or null if not present.
     * @param ssp scheme specific part
     * @return node value or null
     */
    private static String getNodeOrNull(String ssp) {
        return getValueOrNull(ssp, NODE);
    }


    /**
     * Return the value of the 'item' key or null if not present.
     * @param ssp scheme specific part
     * @return item value or null
     */
    private static String getItemOrNull(String ssp) {
        return getValueOrNull(ssp, ITEM);
    }


    /**
     * Return the value of the 'action' key or null if not present.
     * @param ssp scheme specific part
     * @return action value or null
     */
    private static Action getActionOrNull(String ssp) {
        String actionString = getValueOrNull(ssp, ACTION);
        if (actionString == null) {
            return null;
        }
        if (!ssp.substring(ssp.indexOf("?") + 1).startsWith(PUBSUB)) {
            LOGGER.log(Level.WARNING, "URI contains 'action', but does not have iquerytype set to '" + PUBSUB + "'.\n" +
                    "See https://tools.ietf.org/html/rfc5122#section-2.2 and https://xmpp.org/extensions/xep-0060.html#example-229 f.");
        }
        return Action.valueOf(actionString);
    }

    /**
     * Returns the ivalue value of the ipair with ikey key.
     *
     * @see <a href="https://tools.ietf.org/html/rfc5122#section-2.2">ABNF in section 2.2 of rfc5122</a>
     * @param ssp scheme specific part of the XMPP URI/IRI.
     * @param key ikey
     * @return ivalue
     */
    private static String getValueOrNull(String ssp, String key) {
        String args = ssp.substring(ssp.indexOf("?") + 1);
        boolean hasKey = args.contains(key);
        if (!hasKey) return null;

        int keyPos = args.indexOf(key) + key.length();
        int nextPos = args.indexOf(";", keyPos);
        if (nextPos == -1) {
            nextPos = args.length();
        }
        return args.substring(keyPos, nextPos);
    }

    @Override
    public String toString() {
        return SCHEME + ":" + getService() + "?" +
                (getAction() != null ? PUBSUB + ACTION + getAction() : "") +
                (getNode() != null ? NODE + getNode() : "") +
                (getItem() != null ? ITEM + getItem() : "");
    }

    /**
     * PubSub specific actions as defined in XEP-0060: Publish-Subscribe: §16.6 URI Query Types.
     *
     * @see <a href="https://xmpp.org/extensions/xep-0060.html#registrar-querytypes">Registered URI Query Types</a>
     */
    public enum Action {
        subscribe,
        unsubscribe,
        retrieve
    }
}
