/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     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.apache.dubbo.common.url.component;

import org.apache.dubbo.common.utils.NetUtils;
import org.apache.dubbo.common.utils.StringUtils;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Objects;

import static org.apache.dubbo.common.constants.CommonConstants.PATH_SEPARATOR;

public class URLAddress implements Serializable {
    private static final long serialVersionUID = -1985165475234910535L;

    protected String host;
    protected int port;

    // cache
    protected transient String rawAddress;
    protected transient long timestamp;

    public URLAddress(String host, int port) {
        this(host, port, null);
    }

    public URLAddress(String host, int port, String rawAddress) {
        this.host = host;
        port = Math.max(port, 0);
        this.port = port;

        this.rawAddress = rawAddress;
        this.timestamp = System.currentTimeMillis();
    }

    public String getProtocol() {
        return "";
    }

    public URLAddress setProtocol(String protocol) {
        return this;
    }

    public String getUsername() {
        return "";
    }

    public URLAddress setUsername(String username) {
        return this;
    }

    public String getPassword() {
        return "";
    }

    public URLAddress setPassword(String password) {
        return this;
    }

    public String getPath() {
        return "";
    }

    public URLAddress setPath(String path) {
        return this;
    }

    public String getHost() {
        return host;
    }

    public URLAddress setHost(String host) {
        return new URLAddress(host, port, rawAddress);
    }

    public int getPort() {
        return port;
    }

    public URLAddress setPort(int port) {
        return new URLAddress(host, port, rawAddress);
    }

    public String getAddress() {
        if (rawAddress == null) {
            rawAddress = getAddress(getHost(), getPort());
        }
        return rawAddress;
    }

    public URLAddress setAddress(String host, int port) {
        return new URLAddress(host, port, rawAddress);
    }

    public String getIp() {
        return NetUtils.getIpByHost(getHost());
    }

    public String getRawAddress() {
        return rawAddress;
    }

    protected String getAddress(String host, int port) {
        return port <= 0 ? host : host + ':' + port;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }

    @Override
    public int hashCode() {
        return Objects.hash(host, port);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof URLAddress)) return false;
        URLAddress that = (URLAddress) obj;
        return Objects.equals(this.getProtocol(), that.getProtocol()) &&
                Objects.equals(this.getUsername(), that.getUsername()) &&
                Objects.equals(this.getPassword(), that.getPassword()) &&
                Objects.equals(this.getPath(), that.getPath()) &&
                Objects.equals(this.getHost(), that.getHost()) &&
                Objects.equals(this.getPort(), that.getPort());
    }

    @Override
    public String toString() {
        if (rawAddress != null) {
            return rawAddress;
        }

        StringBuilder buf = new StringBuilder();
        if (StringUtils.isNotEmpty(host)) {
            buf.append(host);
            if (port > 0) {
                buf.append(':');
                buf.append(port);
            }
        }
        return buf.toString();
    }

    public static URLAddress parse(String rawAddress, String defaultProtocol, boolean encoded) {
        try {
            String decodeStr = rawAddress;
            if (encoded) {
                decodeStr = URLDecoder.decode(rawAddress, "UTF-8");
            }

            boolean isPathAddress = decodeStr.contains(PATH_SEPARATOR);
            if (isPathAddress) {
                return createPathURLAddress(decodeStr, rawAddress, defaultProtocol);
            }
            return createURLAddress(decodeStr, rawAddress, defaultProtocol);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    private static URLAddress createURLAddress(String decodeStr, String rawAddress, String defaultProtocol) {
        String host = null;
        int port = 0;

        int i = decodeStr.lastIndexOf(':');
        if (i >= 0 && i < decodeStr.length() - 1) {
            if (decodeStr.lastIndexOf('%') > i) {
                // ipv6 address with scope id
                // e.g. fe80:0:0:0:894:aeec:f37d:23e1%en0
                // see https://howdoesinternetwork.com/2013/ipv6-zone-id
                // ignore
            } else {
                port = Integer.parseInt(decodeStr.substring(i + 1));
                host = decodeStr.substring(0, i);
            }
        } else {
            host = decodeStr;
        }

        return new URLAddress(host, port, rawAddress);
    }

    private static PathURLAddress createPathURLAddress(String decodeStr, String rawAddress, String defaultProtocol) {
        String protocol = defaultProtocol;
        String path = null, username = null, password = null, host = null;
        int port = 0;
        int i = decodeStr.indexOf("://");
        if (i >= 0) {
            if (i == 0) {
                throw new IllegalStateException("url missing protocol: \"" + decodeStr + "\"");
            }
            protocol = decodeStr.substring(0, i);
            decodeStr = decodeStr.substring(i + 3);
        } else {
            // case: file:/path/to/file.txt
            i = decodeStr.indexOf(":/");
            if (i >= 0) {
                if (i == 0) {
                    throw new IllegalStateException("url missing protocol: \"" + decodeStr + "\"");
                }
                protocol = decodeStr.substring(0, i);
                decodeStr = decodeStr.substring(i + 1);
            }
        }

        i = decodeStr.indexOf('/');
        if (i >= 0) {
            path = decodeStr.substring(i + 1);
            decodeStr = decodeStr.substring(0, i);
        }
        i = decodeStr.lastIndexOf('@');
        if (i >= 0) {
            username = decodeStr.substring(0, i);
            int j = username.indexOf(':');
            if (j >= 0) {
                password = username.substring(j + 1);
                username = username.substring(0, j);
            }
            decodeStr = decodeStr.substring(i + 1);
        }
        i = decodeStr.lastIndexOf(':');
        if (i >= 0 && i < decodeStr.length() - 1) {
            if (decodeStr.lastIndexOf('%') > i) {
                // ipv6 address with scope id
                // e.g. fe80:0:0:0:894:aeec:f37d:23e1%en0
                // see https://howdoesinternetwork.com/2013/ipv6-zone-id
                // ignore
            } else {
                port = Integer.parseInt(decodeStr.substring(i + 1));
                host = decodeStr.substring(0, i);
            }
        }

        // check cache
        protocol = URLItemCache.intern(protocol);
        path = URLItemCache.checkPath(path);

        return new PathURLAddress(protocol, username, password, path, host, port, rawAddress);
    }
}
