/*
 * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.webkit.network;

import java.net.URI;
import java.text.ParseException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * An RFC 6265-compliant cookie.
 */
final class Cookie {

    private static final Logger logger =
            Logger.getLogger(Cookie.class.getName());
    private static final Pattern IP_ADDRESS_PATTERN = Pattern.compile(
            "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})");


    private final String name;
    private final String value;
    private final long expiryTime;
    private String domain;
    private String path;
    private ExtendedTime creationTime;
    private long lastAccessTime;
    private final boolean persistent;
    private boolean hostOnly;
    private final boolean secureOnly;
    private final boolean httpOnly;


    /**
     * Creates a new {@code Cookie}.
     */
    private Cookie(String name, String value, long expiryTime, String domain,
            String path, ExtendedTime creationTime, long lastAccessTime,
            boolean persistent, boolean hostOnly, boolean secureOnly,
            boolean httpOnly)
    {
        this.name = name;
        this.value = value;
        this.expiryTime = expiryTime;
        this.domain = domain;
        this.path = path;
        this.creationTime = creationTime;
        this.lastAccessTime = lastAccessTime;
        this.persistent = persistent;
        this.hostOnly = hostOnly;
        this.secureOnly = secureOnly;
        this.httpOnly = httpOnly;
    }


    /**
     * Parses a {@code Set-Cookie} header string into a {@code Cookie}
     * object.
     */
    static Cookie parse(String setCookieString, ExtendedTime currentTime) {
        logger.log(Level.FINEST, "setCookieString: [{0}]", setCookieString);

        String[] items = setCookieString.split(";", -1);

        String[] nameValuePair = items[0].split("=", 2);
        if (nameValuePair.length != 2) {
            logger.log(Level.FINEST, "Name-value pair string lacks '=', "
                    + "ignoring cookie");
            return null;
        }
        String name = nameValuePair[0].trim();
        String value = nameValuePair[1].trim();
        if (name.length() == 0) {
            logger.log(Level.FINEST, "Name string is empty, ignoring cookie");
            return null;
        }

        Long expires = null;
        Long maxAge = null;
        String domain = null;
        String path = null;
        boolean secure = false;
        boolean httpOnly = false;

        for (int i = 1; i < items.length; i++) {
            String[] terms = items[i].split("=", 2);
            String attrName = terms[0].trim();
            String attrValue = (terms.length > 1 ? terms[1] : "").trim();

            try {
                if ("Expires".equalsIgnoreCase(attrName)) {
                    expires = parseExpires(attrValue);
                } else if ("Max-Age".equalsIgnoreCase(attrName)) {
                    maxAge = parseMaxAge(attrValue, currentTime.baseTime());
                } else if ("Domain".equalsIgnoreCase(attrName)) {
                    domain = parseDomain(attrValue);
                } else if ("Path".equalsIgnoreCase(attrName)) {
                    path = parsePath(attrValue);
                } else if ("Secure".equalsIgnoreCase(attrName)) {
                    secure = true;
                } else if ("HttpOnly".equalsIgnoreCase(attrName)) {
                    httpOnly = true;
                } else {
                    logger.log(Level.FINEST, "Unknown attribute: [{0}], "
                            + "ignoring", attrName);
                }
            } catch (ParseException ex) {
                logger.log(Level.FINEST, "{0}, ignoring", ex.getMessage());
            }
        }

        long expiryTime;
        boolean persistent;
        if (maxAge != null) {
            persistent = true;
            expiryTime = maxAge;
        } else if (expires != null) {
            persistent = true;
            expiryTime = expires;
        } else {
            persistent = false;
            expiryTime = Long.MAX_VALUE;
        }

        if (domain == null) {
            domain = "";
        }

        Cookie result = new Cookie(name, value, expiryTime, domain, path,
                currentTime, currentTime.baseTime(), persistent, false,
                secure, httpOnly);
        logger.log(Level.FINEST, "result: {0}", result);
        return result;
    }

    /**
     * Parses the value of the {@code Expires} attribute.
     */
    private static long parseExpires(String attributeValue)
        throws ParseException
    {
        try {
            return Math.max(DateParser.parse(attributeValue), 0);
        } catch (ParseException ex) {
            throw new ParseException("Error parsing Expires attribute", 0);
        }
    }

    /**
     * Parses the value of the {@code Max-Age} attribute.
     */
    private static long parseMaxAge(String attributeValue, long currentTime)
        throws ParseException
    {
        try {
            long maxAge = Long.parseLong(attributeValue);
            if (maxAge <= 0) {
                return 0;
            } else {
                return maxAge > (Long.MAX_VALUE - currentTime) / 1000
                        ? Long.MAX_VALUE : currentTime + maxAge * 1000;
            }
        } catch (NumberFormatException ex) {
            throw new ParseException("Error parsing Max-Age attribute", 0);
        }
    }

    /**
     * Parses the value of the {@code Domain} attribute.
     */
    private static String parseDomain(String attributeValue)
        throws ParseException
    {
        if (attributeValue.length() == 0) {
            throw new ParseException("Domain attribute is empty", 0);
        }
        if (attributeValue.startsWith(".")) {
            attributeValue = attributeValue.substring(1);
        }
        return attributeValue.toLowerCase();
    }

    /**
     * Parses the value of the {@code Path} attribute.
     */
    private static String parsePath(String attributeValue) {
        return attributeValue.startsWith("/") ? attributeValue : null;
    }


    /**
     * Returns the name of this cookie.
     */
    String getName() {
        return name;
    }

    /**
     * Returns the value of this cookie.
     */
    String getValue() {
        return value;
    }

    /**
     * Returns the expiry time of this cookie.
     */
    long getExpiryTime() {
        return expiryTime;
    }

    /**
     * Returns the domain of this cookie.
     */
    String getDomain() {
        return domain;
    }

    /**
     * Sets the domain of this cookie.
     */
    void setDomain(String domain) {
        this.domain = domain;
    }

    /**
     * Returns the path of this cookie.
     */
    String getPath() {
        return path;
    }

    /**
     * Sets the path of this cookie.
     */
    void setPath(String path) {
        this.path = path;
    }

    /**
     * Returns the creation time of this cookie.
     */
    ExtendedTime getCreationTime() {
        return creationTime;
    }

    /**
     * Sets the creation time of this cookie.
     */
    void setCreationTime(ExtendedTime creationTime) {
        this.creationTime = creationTime;
    }

    /**
     * Returns the last access time of this cookie.
     */
    long getLastAccessTime() {
        return lastAccessTime;
    }

    /**
     * Sets the last access time of this cookie.
     */
    void setLastAccessTime(long lastAccessTime) {
        this.lastAccessTime = lastAccessTime;
    }

    /**
     * Returns the persistent property of this cookie.
     */
    boolean getPersistent() {
        return persistent;
    }

    /**
     * Returns the host-only property of this cookie.
     */
    boolean getHostOnly() {
        return hostOnly;
    }

    /**
     * Sets the host-only property of this cookie.
     */
    void setHostOnly(boolean hostOnly) {
        this.hostOnly = hostOnly;
    }

    /**
     * Returns the secure-only property of this cookie.
     */
    boolean getSecureOnly() {
        return secureOnly;
    }

    /**
     * Returns the http-only property of this cookie.
     */
    boolean getHttpOnly() {
        return httpOnly;
    }

    /**
     * Determines if this cookie has expired.
     */
    boolean hasExpired() {
        return System.currentTimeMillis() > expiryTime;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Cookie) {
            Cookie cookie = (Cookie) obj;
            return equal(name, cookie.name)
                    && equal(domain, cookie.domain)
                    && equal(path, cookie.path);
        } else {
            return false;
        }
    }

    /**
     * Determines, in null-safe manner, if two objects are equal.
     */
    private static boolean equal(Object obj1, Object obj2) {
        return (obj1 == null && obj2 == null)
                || (obj1 != null && obj1.equals(obj2));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        int hashCode = 7;
        hashCode = 53 * hashCode + hashCode(name);
        hashCode = 53 * hashCode + hashCode(domain);
        hashCode = 53 * hashCode + hashCode(path);
        return hashCode;
    }

    /**
     * Computes the hash code of an object in null safe-manner.
     */
    private static int hashCode(Object obj) {
        return obj != null ? obj.hashCode() : 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "[name=" + name + ", value=" + value + ", "
                + "expiryTime=" + expiryTime + ", domain=" + domain + ", "
                + "path=" + path + ", creationTime=" + creationTime + ", "
                + "lastAccessTime=" + lastAccessTime + ", "
                + "persistent=" + persistent + ", hostOnly=" + hostOnly + ", "
                + "secureOnly=" + secureOnly + ", httpOnly=" + httpOnly + "]";
    }

    /**
     * Determines if a domain matches another domain.
     */
    static boolean domainMatches(String domain, String cookieDomain) {
        return domain.endsWith(cookieDomain) && (
                domain.length() == cookieDomain.length()
                || domain.charAt(domain.length()
                        - cookieDomain.length() - 1) == '.'
                && !isIpAddress(domain));
    }

    /**
     * Determines if a hostname is an IP address.
     */
    private static boolean isIpAddress(String hostname) {
        Matcher matcher = IP_ADDRESS_PATTERN.matcher(hostname);
        if (!matcher.matches()) {
            return false;
        }
        for (int i = 1; i <= matcher.groupCount(); i++) {
            if (Integer.parseInt(matcher.group(i)) > 255) {
                return false;
            }
        }
        return true;
    }

    /**
     * Computes the default path for a given URI.
     */
    static String defaultPath(URI uri) {
        String path = uri.getPath();
        if (path == null || !path.startsWith("/")) {
            return "/";
        }
        path = path.substring(0, path.lastIndexOf("/"));
        if (path.length() == 0) {
            return "/";
        }
        return path;
    }

    /**
     * Determines if a path matches another path.
     */
    static boolean pathMatches(String path, String cookiePath) {
        return path != null && path.startsWith(cookiePath) && (
                path.length() == cookiePath.length()
                || cookiePath.endsWith("/")
                || path.charAt(cookiePath.length()) == '/');
    }
}
