/**
 *
 * Copyright 2019 Florian Schmaus
 *
 * Licensed 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.jivesoftware.smack.util;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;

import org.minidns.dnslabel.DnsLabel;
import org.minidns.dnsname.DnsName;
import org.minidns.dnsname.InvalidDnsNameException;
import org.minidns.util.InetAddressUtil;

/**
 * An internet address, can be given as IP or as DNS name.
 * <p>
 * This type is meant for strings that hold an internet address. The original string used to construct this type is
 * stored and returning in the {@link #toString()} method.
 * </p>
 *
 * @since 4.4.0
 */
public abstract class InternetAddress implements CharSequence {

    protected final String originalString;

    protected InternetAddress(String originalString) {
        this.originalString = Objects.requireNonNull(originalString, "The 'originalString' argument must not be null");
    }

    public abstract InetAddress asInetAddress() throws UnknownHostException;

    @Override
    public String toString() {
        return originalString;
    }

    @Override
    public int length() {
        return originalString.length();
    }

    @Override
    public char charAt(int index) {
        return originalString.charAt(index);
    }

    @Override
    public CharSequence subSequence(int start, int end) {
        return originalString.subSequence(start, end);
    }

    public static InternetAddress from(String address) {
        final InternetAddress internetAddress;
        if (InetAddressUtil.isIpV4Address(address)) {
            internetAddress = new InternetAddress.Ipv4(address);
        } else if (InetAddressUtil.isIpV6Address(address)) {
            internetAddress = new InternetAddress.Ipv6(address);
        } else if (address.contains(".")) {
            InternetAddress domainNameInternetAddress;
            try {
                DnsName dnsName = DnsName.from(address);
                domainNameInternetAddress = new InternetAddress.DomainName(address, dnsName);
            } catch (InvalidDnsNameException e) {
                domainNameInternetAddress = new InternetAddress.InvalidDomainName(address, e);
            }
            internetAddress = domainNameInternetAddress;
        } else {
            DnsLabel dnsLabel = DnsLabel.from(address);
            internetAddress = new InternetAddress.DomainNameLabel(address, dnsLabel);
        }
        return internetAddress;
    }

    public static InternetAddress from(InetAddress inetAddress) {
        if (inetAddress instanceof Inet4Address) {
            return new InternetAddress.Ipv4(inetAddress.getHostAddress(), (Inet4Address) inetAddress);
        } else if (inetAddress instanceof Inet6Address) {
            return new InternetAddress.Ipv6(inetAddress.getHostAddress(), (Inet6Address) inetAddress);
        } else {
            throw new IllegalArgumentException("Unknown type " + inetAddress.getClass() + " of " + inetAddress);
        }
    }

    private static class InetAddressInternetAddress extends InternetAddress {
        private final InetAddress inetAddress;

        protected InetAddressInternetAddress(String originalString, InetAddress inetAddress) {
            super(originalString);
            this.inetAddress = inetAddress;
        }

        @Override
        public InetAddress asInetAddress() {
            return inetAddress;
        }
    }

    public static final class Ipv4 extends InetAddressInternetAddress {

        private final Inet4Address inet4Address;

        private Ipv4(String originalString) {
            this(originalString, InetAddressUtil.ipv4From(originalString));
        }

        private Ipv4(String originalString, Inet4Address inet4Address) {
            super(originalString, inet4Address);
            this.inet4Address = inet4Address;
        }

        public Inet4Address getInet4Address() {
            return inet4Address;
        }
    }

    public static final class Ipv6 extends InetAddressInternetAddress {

        private Inet6Address inet6Address;

        private Ipv6(String originalString) {
            this(originalString, InetAddressUtil.ipv6From(originalString));
        }

        private Ipv6(String originalString, Inet6Address inet6Address) {
            super(originalString, inet6Address);
            this.inet6Address = inet6Address;
        }

        public Inet6Address getInet6Address() {
            return inet6Address;
        }
    }

    private static class NonNumericInternetAddress extends InternetAddress {
        private boolean attemptedToResolveInetAddress;
        private InetAddress inetAddress;

        protected NonNumericInternetAddress(String originalString) {
            super(originalString);
        }

        @Override
        public InetAddress asInetAddress() throws UnknownHostException {
            if (inetAddress != null || attemptedToResolveInetAddress) {
                return inetAddress;
            }

            attemptedToResolveInetAddress = true;
            inetAddress = InetAddress.getByName(originalString);

            return inetAddress;
        }
    }

    public static final class DomainName extends NonNumericInternetAddress {

        private final DnsName dnsName;

        private DomainName(String originalString, DnsName dnsName) {
            super(originalString);
            this.dnsName = dnsName;
        }

        public DnsName getDnsName() {
            return dnsName;
        }

    }

    public static final class DomainNameLabel extends NonNumericInternetAddress {

        private final DnsLabel dnsLabel;

        private DomainNameLabel(String originalString, DnsLabel dnsLabel) {
            super(originalString);
            this.dnsLabel = dnsLabel;
        }

        public DnsLabel getDnsLabel() {
            return dnsLabel;
        }
    }

    public static final class InvalidDomainName extends NonNumericInternetAddress {

        private final InvalidDnsNameException invalidDnsNameException;

        private InvalidDomainName(String originalString, InvalidDnsNameException invalidDnsNameException) {
            super(originalString);
            this.invalidDnsNameException = invalidDnsNameException;
        }

        public InvalidDnsNameException getInvalidDnsNameException() {
            return invalidDnsNameException;
        }
    }
}
