/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.stack.core.types.builtin;

import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableSet;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;

public final class StatusCode {

    private static final long SEVERITY_MASK = 0xC0000000L;
    private static final long SEVERITY_GOOD = 0x00000000L;
    private static final long SEVERITY_UNCERTAIN = 0x40000000L;
    private static final long SEVERITY_BAD = 0x80000000L;

    /**
     * StatusCodes that are considered security-related errors for the purpose of diagnostics.
     */
    private static final ImmutableSet<Long> SECURITY_ERRORS;

    static {
        SECURITY_ERRORS = ImmutableSet.<Long>builder()
            .add(
                StatusCodes.Bad_UserSignatureInvalid,
                StatusCodes.Bad_UserAccessDenied,
                StatusCodes.Bad_SecurityPolicyRejected,
                StatusCodes.Bad_SecurityModeRejected,
                StatusCodes.Bad_SecurityChecksFailed,
                StatusCodes.Bad_SecureChannelTokenUnknown,
                StatusCodes.Bad_SecureChannelIdInvalid,
                StatusCodes.Bad_NoValidCertificates,
                StatusCodes.Bad_IdentityTokenInvalid,
                StatusCodes.Bad_IdentityTokenRejected,
                StatusCodes.Bad_IdentityChangeNotSupported,
                StatusCodes.Bad_CertificateUseNotAllowed,
                StatusCodes.Bad_CertificateUriInvalid,
                StatusCodes.Bad_CertificateUntrusted,
                StatusCodes.Bad_CertificateTimeInvalid,
                StatusCodes.Bad_CertificateRevoked,
                StatusCodes.Bad_CertificateRevocationUnknown,
                StatusCodes.Bad_CertificateIssuerUseNotAllowed,
                StatusCodes.Bad_CertificateIssuerTimeInvalid,
                StatusCodes.Bad_CertificateIssuerRevoked,
                StatusCodes.Bad_CertificateIssuerRevocationUnknown,
                StatusCodes.Bad_CertificateInvalid,
                StatusCodes.Bad_CertificateHostNameInvalid,
                StatusCodes.Bad_ApplicationSignatureInvalid
            )
            .build();
    }

    public static final StatusCode GOOD = new StatusCode(SEVERITY_GOOD);
    public static final StatusCode BAD = new StatusCode(SEVERITY_BAD);
    public static final StatusCode UNCERTAIN = new StatusCode(SEVERITY_UNCERTAIN);

    private final long value;

    public StatusCode(int value) {
        this.value = value & 0xFFFFFFFFL;
    }

    public StatusCode(long value) {
        this.value = value & 0xFFFFFFFFL;
    }

    public StatusCode(UInteger value) {
        this(value.longValue());
    }

    public long getValue() {
        return value;
    }

    public boolean isGood() {
        return (value & SEVERITY_MASK) == SEVERITY_GOOD;
    }

    public boolean isBad() {
        return (value & SEVERITY_MASK) == SEVERITY_BAD;
    }

    public boolean isUncertain() {
        return (value & SEVERITY_MASK) == SEVERITY_UNCERTAIN;
    }

    /**
     * Set the DataValue InfoType and Overflow InfoBits.
     *
     * @return a new {@link StatusCode} DataValue and Overflow bits set.
     */
    public StatusCode withOverflow() {
        return new StatusCode(value | 0x480);
    }

    /**
     * Clear the DataValue InfoType and Overflow InfoBits.
     *
     * @return a new {@link StatusCode} with DataValue and Overflow bits cleared.
     */
    public StatusCode withoutOverflow() {
        return new StatusCode(value & ~0x480);
    }

    /**
     * @return {@code true} if DataValue and Overflow bits are set.
     */
    public boolean isOverflowSet() {
        return (value & 0x480) == 0x480;
    }

    /**
     * @return {@code true} if the StatusCode is considered security-related for the purpose of diagnostics.
     */
    public boolean isSecurityError() {
        return SECURITY_ERRORS.contains(value);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        StatusCode that = (StatusCode) o;

        return value == that.value;
    }

    @Override
    public int hashCode() {
        return (int) (value ^ (value >>> 32));
    }

    @Override
    public String toString() {
        ToStringHelper helper = MoreObjects.toStringHelper(this);

        StatusCodes.lookup(value).ifPresent(
            nameAndDesc -> helper.add("name", nameAndDesc[0]));

        helper.add("value", String.format("0x%08X", value));
        helper.add("quality", quality(this));

        return helper.toString();
    }

    private static String quality(StatusCode statusCode) {
        if (statusCode.isGood()) {
            return "good";
        } else if (statusCode.isBad()) {
            return "bad";
        } else if (statusCode.isUncertain()) {
            return "uncertain";
        } else {
            return "unknown";
        }
    }

}
