/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * 
 *      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 org.eclipse.webdav.internal.utils;

import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.StringTokenizer;
import org.eclipse.webdav.Policy;
import org.eclipse.webdav.internal.kernel.utils.Assert;

/**
 * Decodes a <code>URL</code> from an <code>ASCII</code> readable
 * <code>URL</code> that is safe for transport.
 *
 * @see URLEncoder
 */
public final class URLDecoder {

    private static final byte[] hexChars = {(byte) '0', //
            (byte) '1', //
            (byte) '2', //
            (byte) '3', //
            (byte) '4', //
            (byte) '5', //
            (byte) '6', //
            (byte) '7', //
            (byte) '8', //
            (byte) '9', //
            (byte) 'a', //
            (byte) 'b', //
            (byte) 'c', //
            (byte) 'd', //
            (byte) 'e', //
            (byte) 'f', //
            (byte) 'A', //
            (byte) 'B', //
            (byte) 'C', //
            (byte) 'D', //
            (byte) 'E', //
            (byte) 'F'};
    private static final byte[] hexCharValues = {(byte) ('0' - '0'), //
            (byte) ('1' - '0'), //
            (byte) ('2' - '0'), //
            (byte) ('3' - '0'), //
            (byte) ('4' - '0'), //
            (byte) ('5' - '0'), //
            (byte) ('6' - '0'), //
            (byte) ('7' - '0'), //
            (byte) ('8' - '0'), //
            (byte) ('9' - '0'), //
            (byte) ('a' - 'a' + 10), //
            (byte) ('b' - 'a' + 10), //
            (byte) ('c' - 'a' + 10), //
            (byte) ('d' - 'a' + 10), //
            (byte) ('e' - 'a' + 10), //
            (byte) ('f' - 'a' + 10), //
            (byte) ('A' - 'A' + 10), //
            (byte) ('B' - 'A' + 10), //
            (byte) ('C' - 'A' + 10), //
            (byte) ('D' - 'A' + 10), //
            (byte) ('E' - 'A' + 10), //
            (byte) ('F' - 'A' + 10)};

    /**
     * Prevents instances from being created.
     */
    private URLDecoder() {
        super();
    }

    /**
     * Decodes the given <code>URL</code> from an <code>ASCII</code>
     * readable <code>URL</code> that is safe for transport. Returns the
     * result.
     *
     * @return the result of decoding the given <code>URL</code> from an
     *         <code>ASCII</code> readable <code>URL</code> that is safe for
     *         transport
     */
    public static String decode(String url) {
        try {
            return decode(new URL(url)).toString();
        } catch (MalformedURLException e) {
            // ignore or log?
        }

        String file;
        String ref = null;

        int lastSlashIndex = url.lastIndexOf('/');
        int lastHashIndex = url.lastIndexOf('#');
        if ((lastHashIndex - lastSlashIndex > 1) && lastHashIndex < url.length() - 1) {
            file = url.substring(0, lastHashIndex);
            ref = url.substring(lastHashIndex + 1, url.length());
        } else {
            file = url;
        }

        return decode(file, ref);
    }

    /**
     * Decodes the file and reference parts of a <code>URL</code> from an
     * <code>ASCII</code> readable <code>URL</code> that is safe for
     * transport. Returns the result.
     *
     * @return the result of decoding the file and reference parts of a
     *         <code>URL</code> from an <code>ASCII</code> readable
     *         <code>URL</code> that is safe for transport
     */
    public static String decode(String file, String ref) {
        StringBuffer buf = new StringBuffer();
        StringTokenizer tokenizer = new StringTokenizer(file, "/", true); //$NON-NLS-1$

        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            if (token.equals("/")) { //$NON-NLS-1$
                buf.append(token);
            } else {
                buf.append(decodeSegment(token));
            }
        }

        if (ref != null) {
            buf.append('#');
            buf.append(decodeSegment(ref));
        }

        return buf.toString();
    }

    /**
     * Decodes the given <code>URL</code> from an <code>ASCII</code>
     * readable <code>URL</code> that is safe for transport. Returns the
     * result.
     *
     * @return the result of decoding the given <code>URL</code> from an
     *         <code>ASCII</code> readable <code>URL</code> that is safe for
     *         transport
     */
    public static URL decode(URL url) {
        String file = url.getFile();
        String ref = url.getRef();
        try {
            return new URL(url.getProtocol(), url.getHost(), url.getPort(), decode(file, ref));
        } catch (MalformedURLException e) {
            Assert.isTrue(false, Policy.bind("assert.internalError")); //$NON-NLS-1$
        }
        return null;
    }

    public static String decodeSegment(String segment) {
        byte[] encodedBytes = segment.getBytes();
        byte[] decodedBytes = new byte[encodedBytes.length];
        int decodedLength = 0;

        for (int i = 0; i < encodedBytes.length; i++) {
            byte b = encodedBytes[i];
            try {
                if (b == '%') {
                    byte enc1 = encodedBytes[++i];
                    byte enc2 = encodedBytes[++i];
                    b = (byte) ((hexToByte(enc1) << 4) + hexToByte(enc2));
                }
                decodedBytes[decodedLength++] = b;
            } catch (ArrayIndexOutOfBoundsException e) {
                Assert.isTrue(false, Policy.bind("assert.decodeSegment")); //$NON-NLS-1$
            } catch (IllegalArgumentException e) {
                Assert.isTrue(false, Policy.bind("assert.decodeSegment")); //$NON-NLS-1$
            }
        }
        try {
            return new String(decodedBytes, 0, decodedLength, "UTF8"); //$NON-NLS-1$
        } catch (UnsupportedEncodingException exception) {
            Assert.isTrue(false, Policy.bind("assert.internalError")); //$NON-NLS-1$
            // avoid compiler error
            return null;
        }
    }

    private final static byte hexToByte(byte ch) {
        for (int i = 0; i < hexChars.length; i++)
            if (hexChars[i] == ch)
                return hexCharValues[i];
        throw new IllegalArgumentException();
    }
}
