/*
 * JacORB - a free Java ORB
 *
 * Copyright (C) 1997-2014 Gerald Brose / The JacORB Team.
 *
 * This library is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Library General Public License as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License along with this
 * library; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139,
 * USA.
 */

package org.jacorb.orb.util;

import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import org.jacorb.orb.ORB;
import org.jacorb.orb.ParsedIOR;
import org.jacorb.orb.iiop.IIOPAddress;
import org.jacorb.orb.iiop.IIOPProfile;
import org.jacorb.orb.miop.MIOPProfile;
import org.omg.ETF.Factories;
import org.omg.ETF.Profile;
import org.omg.GIOP.Version;
import org.omg.MIOP.UIPMC_ProfileBody;
import org.omg.PortableGroup.TagGroupTaggedComponent;

/**
 * @author Gerald Brose
 */
public class CorbaLoc
{
    private final ORB orb;
    private String keyString;
    private byte[] key;
    private String bodyString;
    private boolean is_rir;

    public Profile[] profileList;

    public CorbaLoc(ORB orb, String addr)
    {
        this.orb = orb;
        is_rir = false;
        parse(addr);
    }

    public boolean rir()
    {
        return is_rir;
    }

    @Override
    public String toString()
    {
        return "corbaloc:" + body();
    }

    private String body()
    {
        StringBuffer buffer = new StringBuffer();

        buffer.append(bodyString);

        if (keyString != null)
        {
            buffer.append('/');
            buffer.append(keyString);
        }

        return buffer.toString();
    }

    public String getKeyString()
    {
        return keyString;
    }

    public byte[] getKey()
    {
        return key;
    }

    private void defaultKeyString(String defaultKey)
    {
        if (keyString == null)
        {
            keyString = defaultKey;
        }
        else
        {
            throw new IllegalStateException("KeyString not empty, cannot default to " + defaultKey);
        }
    }

    public String toCorbaName(String str_name)
    {
        if (getKeyString() == null)
        {
            defaultKeyString("NameService");
        }

        if (str_name != null && str_name.length() > 0)
        {
            try
            {
                return "corbaname:" + body() + "#" + str_name;
            }
            catch (Exception e)
            {
                return null;
            }
        }

        return "corbaname:" + body();
    }

    /**
     * parses a string representing a corbaloc: reference
     */
    private void parse(String addr)
    {
        if (addr == null || !addr.startsWith("corbaloc:"))
        {
            throw new IllegalArgumentException("URL must start with \'corbaloc:\'");
        }

        String sb;
        boolean isMIOP = (addr.indexOf("miop") != -1);
        if (isMIOP && addr.indexOf(",iiop") != -1)
        {
            throw new IllegalArgumentException("MIOP Profile does not support Gateway Profiles.");
        }

        if (!isMIOP && addr.indexOf('/') == -1)
        {
            sb = addr.substring(addr.indexOf(':') + 1);
            if (addr.startsWith("corbaloc:rir:"))
            {
                is_rir = true;
                // default key string for rir protocol
                keyString = "NameService";
            }
            else
            {
                keyString = null;
            }
            key = new byte[0];
        }
        else
        {
            String iiopKey;
            int startIndex;
            int endIndex;
            // MIOP plus another e.g. IIOP profile. Scan IIOP profile for key.
            if (isMIOP && addr.indexOf(';') != -1)
            {
                iiopKey = addr.substring(addr.indexOf(';'));
                startIndex = 1;
                endIndex = iiopKey.indexOf('/');
            }
            else
            {
                iiopKey = addr;
                startIndex = iiopKey.indexOf(':') + 1;
                endIndex = (isMIOP ? iiopKey.length() : iiopKey.indexOf('/'));
            }

            sb = iiopKey.substring(startIndex, endIndex);
            keyString = iiopKey.substring(iiopKey.indexOf('/') + 1);
            key = parseKey(keyString);
        }

        // ! MIOP as we don't currently support gateway profiles.
        if (!isMIOP && sb.indexOf(',') > 0)
        {
            StringTokenizer tokenizer = new StringTokenizer(sb, ",");
            profileList = new Profile[tokenizer.countTokens()];
            int pIndex = 0;
            for (int i = 0; i < profileList.length; i++)
            {
                Profile p = parseAddress(tokenizer.nextToken());
                if (p == null)
                {
                    continue;
                }
                profileList[pIndex] = p;
                pIndex++;
            }
            while (pIndex < profileList.length)
            {
                profileList[pIndex] = null;
                pIndex++;
            }

        }
        else
        {
            profileList = new Profile[] { parseAddress(sb) };
        }

        bodyString = sb;
    }

    private Profile parseAddress(String addr)
    {
        int colon = addr.indexOf(':');
        if (colon == -1)
        {
            throw new IllegalArgumentException("Illegal object address format: " + addr);
        }

        if ("rir:".equals(addr))
        {
            is_rir = true;
            /* resolve initials references protocol */
            return null;
        }

        Profile result = null;
        if (orb == null && (colon == 0 || addr.startsWith("iiop:") || addr.startsWith("ssliop:")))
        {
            result = new IIOPProfile(addr);
        }
        else if (orb != null)
        {
            List<Factories> factories = orb.getTransportManager().getFactoriesList();
            for (Iterator<Factories> i = factories.iterator(); result == null && i.hasNext();)
            {
                org.omg.ETF.Factories f = i.next();
                result = f.decode_corbaloc(addr);
            }
        }
        if (result == null)
        {
            throw new IllegalArgumentException("Unknown protocol in object address format: " + addr);
        }
        return result;
    }

    private static boolean legalChar(char c)
    {
        if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
        {
            return true;
        }
        return (c == ';' || c == '/' || c == ':' || c == '?' || c == '@' || c == '&' || c == '=' || c == '+'
                || c == '$' || c == ',' || c == '_' || c == '.' || c == '!' || c == '~' || c == '*' || c == '\''
                || c == '-' || c == '(' || c == ')');
    }

    private static byte hexValue(char c)
    {
        return (byte) ((c >= 'a') ? (10 + c - 'a') : ((c >= 'A') ? (10 + c - 'A') : (c - '0')));
    }

    private static char hexDigit(byte b)
    {
        if ((b & 0xf0) != 0)
        {
            throw new IllegalArgumentException("Hex digit out of range " + b);
        }

        return (char) (b < 10 ? '0' + (char) b : 'A' + (char) b - 10);
    }

    private static boolean isHex(char c)
    {
        return ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'));
    }

    public static byte[] parseKey(String s)
    {
        char[] tmp = s.toCharArray();
        int count = tmp.length;

        for (int i = 0; i < tmp.length; i++)
        {
            if (!legalChar(tmp[i]))
            {
                if (tmp[i] == '%')
                {
                    if (isHex(tmp[i + 1]) && isHex(tmp[i + 2]))
                    {
                        count -= 2;
                        i += 2;
                    }
                    else
                    {
                        throw new IllegalArgumentException("Illegal escape in URL character");
                    }
                }
                else
                {
                    throw new IllegalArgumentException("URL character out of range: " + tmp[i]);
                }
            }
        }

        byte[] result = new byte[count];
        int idx = 0;

        for (int i = 0; i < count; i++)
        {
            if (legalChar(tmp[idx]))
            {
                result[i] = (byte) tmp[idx++];
            }
            else
            {
                result[i] = (byte) ((hexValue(tmp[idx + 1])) << 4 | hexValue(tmp[idx + 2]));
                idx += 3;
            }
        }
        return result;
    }

    public static String parseKey(byte[] key)
    {
        StringBuffer buffer = new StringBuffer();

        for (int i = 0; i < key.length; i++)
        {
            if (!legalChar((char) key[i]))
            {
                buffer.append('%');
                // Mask the bytes before shift to ensure 10001001 doesn't get
                // shifted to 11111000 but 00001000 (linden java faq).
                buffer.append(hexDigit((byte) ((key[i] & 0xff) >> 4)));
                buffer.append(hexDigit((byte) (key[i] & 0x0f)));
            }
            else
            {
                buffer.append((char) key[i]);
            }
        }
        return buffer.toString();
    }

    public static String generateCorbaloc(org.omg.CORBA.ORB orb, org.omg.CORBA.Object ref)
    {
        ParsedIOR pior = new ParsedIOR((org.jacorb.orb.ORB) orb, orb.object_to_string(ref));

        Profile profile = pior.getEffectiveProfile();

        if (profile instanceof IIOPProfile)
        {
            return createCorbalocForIIOPProfile((IIOPProfile) profile);
        }
        else if (profile instanceof MIOPProfile)
        {
            return createCorbalocForMIOPProfile((MIOPProfile) profile);
        }
        else
        {
            throw new IllegalArgumentException("Profile type not suported: tag number=" + profile.tag());
        }
    }

    /**
     * Create a corbaloc string for a MIOP profile.
     *
     * @param profile
     *            the MIOP profile
     * @return the created crobaloc string
     */
    private static String createCorbalocForMIOPProfile(MIOPProfile profile)
    {
        StringBuffer sb = new StringBuffer("miop:");

        sb.append(createString(profile.version()));
        sb.append("@");
        sb.append(createString(profile.getTagGroup()));
        sb.append("/");
        sb.append(createString(profile.getUIPMCProfile()));

        // group's IIOP component
        sb.append(";");
        sb.append(createCorbalocForIIOPProfile(profile.getGroupIIOPProfile()));

        return sb.toString();
    }

    /**
     * Returns a String version of the tag_group.
     *
     * @param groupInfo
     * @return the created corbaloc string
     */
    private static String createString(TagGroupTaggedComponent groupInfo)
    {
        StringBuffer sb = new StringBuffer();
        sb.append(createString(groupInfo.group_version));
        sb.append("-");
        sb.append(groupInfo.group_domain_id);
        sb.append("-");
        sb.append(groupInfo.object_group_id);
        if (groupInfo.object_group_ref_version != 0)
        {
            sb.append("-");
            sb.append(groupInfo.object_group_ref_version);
        }
        return sb.toString();
    }

    /**
     * Returns a String version of the uipmc profile address.
     *
     * @param uipmc
     * @return the created crobaloc string
     */
    private static String createString(UIPMC_ProfileBody uipmc)
    {
        StringBuffer sb = new StringBuffer();
        sb.append(uipmc.the_address);
        sb.append(":");
        sb.append(uipmc.the_port);
        return sb.toString();
    }

    /**
     * Returns a String version of the Version.
     *
     * @param version
     * @return the created corbaloc string
     */
    private static String createString(Version version)
    {
        StringBuffer sb = new StringBuffer();
        sb.append(version.major);
        sb.append(".");
        sb.append(version.minor);
        return sb.toString();
    }

    public static String generateCorbalocForMultiIIOPProfiles(org.omg.CORBA.ORB orb, org.omg.CORBA.Object ref)
    {
        return generateCorbalocForMultiIIOPProfiles(orb, orb.object_to_string(ref));
    }

    public static String generateCorbalocForMultiIIOPProfiles(org.omg.CORBA.ORB orb, String ref)
    {
        ParsedIOR pior = new ParsedIOR((org.jacorb.orb.ORB) orb, ref);

        String result = null;
        String object_key = null;
        Iterator<Profile> iterator;
        for (iterator = pior.getProfiles().iterator(); iterator.hasNext();)
        {
            Profile profile = iterator.next();

            if (profile instanceof IIOPProfile)
            {
                String s = createCorbalocForIIOPProfile((IIOPProfile) profile, true);
                if (result != null)
                {
                    result += "," + s;
                }
                else
                {
                    result = "corbaloc:" + s;
                    object_key = parseKey(profile.get_object_key());
                }

            }
        }
        if (result != null)
        {
            result += "/" + object_key;
        }

        return result;
    }

    public static String createCorbalocForIIOPProfile(IIOPProfile profile)
    {
        return createCorbalocForIIOPProfile(profile, false).concat('/' + parseKey(profile.get_object_key()));
    }

    /**
     * Returns a iiop list ; note this function does not add the object_key.
     *
     * @param profile
     * @param addAlternates
     *            boolean whether to add alternate profiles to this corbaloc
     * @return
     */
    private static String createCorbalocForIIOPProfile(IIOPProfile profile, boolean addAlternates)
    {
        StringBuffer sb = new StringBuffer("iiop:");
        sb.append(createString(profile.version()));
        sb.append("@");
        sb.append(wrapIPv6(((IIOPAddress) profile.getAddress()).getOriginalHost()));
        sb.append(":");
        sb.append(((IIOPAddress) profile.getAddress()).getPort());

        for (Iterator<IIOPAddress> iter = profile.getAlternateAddresses().iterator(); addAlternates && iter.hasNext();)
        {
            IIOPAddress address = iter.next();
            sb.append(",iiop:");
            sb.append(createString(profile.version()));
            sb.append("@");
            sb.append(wrapIPv6(address.getOriginalHost()));
            sb.append(":");
            sb.append(address.getPort());
        }

        return sb.toString();
    }

    private static String wrapIPv6(String addr)
    {
        String result = addr;

        InetAddress address;
        try
        {
            address = InetAddress.getByName(addr);

            if (address instanceof Inet6Address)
            {
                result = '[' + result + ']';
            }
        }
        catch (UnknownHostException e)
        {
        }

        return result;

    }

    public static void main(String[] args)
    {
        String[] noarg = new String[] {};
        ORB orb = (org.jacorb.orb.ORB) ORB.init(noarg, null);
        for (int i = 0; i < args.length; i++)
        {
            System.out.println(new CorbaLoc(orb, args[i]).toString());
        }
    }
}
