/*
 * Copyright 2017 The Apache Software Foundation.
 *
 * 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.apache.bookkeeper.client.api;

import java.lang.reflect.Field;
import java.util.function.Function;
import org.apache.bookkeeper.client.LedgerHandleAdv;
import org.apache.bookkeeper.common.annotation.InterfaceAudience.Public;
import org.apache.bookkeeper.common.annotation.InterfaceStability.Unstable;

/**
 * Super class for all errors which occur using BookKeeper client.
 *
 * @since 4.6
 */
@Public
@Unstable
public class BKException extends Exception {
    static final Function<Throwable, BKException> HANDLER = cause -> {
        if (cause == null) {
            return null;
        }
        if (cause instanceof BKException) {
            return (BKException) cause;
        } else {
            BKException ex = new BKException(Code.UnexpectedConditionException);
            ex.initCause(cause);
            return ex;
        }
    };

    protected final int code;

    private static final LogMessagePool logMessagePool = new LogMessagePool();

    /**
     * Create a new exception.
     *
     * @param code the error code
     *
     * @see Code
     */
    public BKException(int code) {
        super(getMessage(code));
        this.code = code;
    }

    /**
     * Create a new exception with the <tt>cause</tt>.
     *
     * @param code exception code
     * @param cause the exception cause
     */
    public BKException(int code, Throwable cause) {
        super(getMessage(code), cause);
        this.code = code;
    }

    /**
     * Get the return code for the exception.
     *
     * @return the error code
     *
     * @see Code
     */
    public final int getCode() {
        return this.code;
    }

    /**
     * Returns a lazy error code formatter suitable to pass to log functions.
     *
     * @param code the error code value
     *
     * @return lazy error code log formatter
     */
    public static Object codeLogger(int code) {
        return logMessagePool.get(code);
    }

    /**
     * Describe an error code.
     *
     * @param code the error code value
     *
     * @return the description of the error code
     */
    public static String getMessage(int code) {
        switch (code) {
        case Code.OK:
            return "No problem";
        case Code.ReadException:
            return "Error while reading ledger";
        case Code.QuorumException:
            return "Invalid quorum size on ensemble size";
        case Code.NoBookieAvailableException:
            return "No bookie available";
        case Code.DigestNotInitializedException:
            return "Digest engine not initialized";
        case Code.DigestMatchException:
            return "Entry digest does not match";
        case Code.NotEnoughBookiesException:
            return "Not enough non-faulty bookies available";
        case Code.NoSuchLedgerExistsException:
            return "No such ledger exists on Bookies";
        case Code.NoSuchLedgerExistsOnMetadataServerException:
            return "No such ledger exists on Metadata Server";
        case Code.BookieHandleNotAvailableException:
            return "Bookie handle is not available";
        case Code.ZKException:
            return "Error while using ZooKeeper";
        case Code.MetaStoreException:
            return "Error while using MetaStore";
        case Code.LedgerExistException:
            return "Ledger existed";
        case Code.LedgerRecoveryException:
            return "Error while recovering ledger";
        case Code.LedgerClosedException:
            return "Attempt to write to a closed ledger";
        case Code.WriteException:
            return "Write failed on bookie";
        case Code.NoSuchEntryException:
            return "No such entry";
        case Code.IncorrectParameterException:
            return "Incorrect parameter input";
        case Code.InterruptedException:
            return "Interrupted while waiting for permit";
        case Code.ProtocolVersionException:
            return "Bookie protocol version on server is incompatible with client";
        case Code.MetadataVersionException:
            return "Bad ledger metadata version";
        case Code.DuplicateEntryIdException:
            return "Attempted to add Duplicate entryId";
        case Code.LedgerFencedException:
            return "Ledger has been fenced off. Some other client must have opened it to read";
        case Code.UnauthorizedAccessException:
            return "Attempted to access ledger using the wrong password";
        case Code.UnclosedFragmentException:
            return "Attempting to use an unclosed fragment; This is not safe";
        case Code.WriteOnReadOnlyBookieException:
            return "Attempting to write on ReadOnly bookie";
        case Code.TooManyRequestsException:
            return "Too many requests to the same Bookie";
        case Code.LedgerIdOverflowException:
            return "Next ledgerID is too large.";
        case Code.ReplicationException:
            return "Errors in replication pipeline";
        case Code.ClientClosedException:
            return "BookKeeper client is closed";
        case Code.IllegalOpException:
            return "Invalid operation";
        case Code.AddEntryQuorumTimeoutException:
            return "Add entry quorum wait timed out";
        case Code.TimeoutException:
            return "Bookie operation timeout";
        case Code.SecurityException:
            return "Failed to establish a secure connection";
        case Code.MetadataSerializationException:
            return "Failed to serialize metadata";
        case Code.DataUnknownException:
            return "Ledger in limbo";
        default:
            return "Unexpected condition";
        }
    }

    /**
     * Codes which represent the various exception types.
     */
    public interface Code {
        /** A placer holder (unused). */
        int UNINITIALIZED = 1;
        /** Everything is OK. */
        int OK = 0;
        /** Read operations failed (bookie error). */
        int ReadException = -1;
        /** Unused. */
        int QuorumException = -2;
        /** Unused. */
        int NoBookieAvailableException = -3;
        /** Digest Manager is not initialized (client error). */
        int DigestNotInitializedException = -4;
        /** Digest doesn't match on returned entries. */
        int DigestMatchException = -5;
        /** Not enough bookies available to form an ensemble. */
        int NotEnoughBookiesException = -6;
        /** No such ledger exists. */
        int NoSuchLedgerExistsException = -7;
        /** Bookies are not available. */
        int BookieHandleNotAvailableException = -8;
        /** ZooKeeper operations failed. */
        int ZKException = -9;
        /** Ledger recovery operations failed. */
        int LedgerRecoveryException = -10;
        /** Executing operations on a closed ledger handle. */
        int LedgerClosedException = -11;
        /** Write operations failed (bookie error). */
        int WriteException = -12;
        /** No such entry exists. */
        int NoSuchEntryException = -13;
        /** Incorrect parameters (operations are absolutely not executed). */
        int IncorrectParameterException = -14;
        /** Synchronous operations are interrupted. */
        int InterruptedException = -15;
        /** Protocol version is wrong (operations are absolutely not executed). */
        int ProtocolVersionException = -16;
        /** Bad version on executing metadata operations. */
        int MetadataVersionException = -17;
        /** Meta store operations failed. */
        int MetaStoreException = -18;
        /** Executing operations on a closed client. */
        int ClientClosedException = -19;
        /** Ledger already exists. */
        int LedgerExistException = -20;
        /**
         * Add entry operation timeouts on waiting quorum responses.
         *
         * @since 4.5
         */
        int AddEntryQuorumTimeoutException = -21;
        /**
         * Duplicated entry id is found when {@link LedgerHandleAdv#addEntry(long, byte[])}.
         *
         * @since 4.5
         */
        int DuplicateEntryIdException = -22;
        /**
         * Operations timeouts.
         *
         * @since 4.5
         */
        int TimeoutException = -23;
        int SecurityException = -24;

        /** No such ledger exists one metadata server. */
        int NoSuchLedgerExistsOnMetadataServerException = -25;

        /**
         * Operation is illegal.
         */
        int IllegalOpException = -100;
        /**
         * Operations failed due to ledgers are fenced.
         */
        int LedgerFencedException = -101;
        /**
         * Operations failed due to unauthorized.
         */
        int UnauthorizedAccessException = -102;
        /**
         * Replication failed due to unclosed fragments.
         */
        int UnclosedFragmentException = -103;
        /**
         * Write operations failed due to bookies are readonly.
         */
        int WriteOnReadOnlyBookieException = -104;
        /**
         * Operations failed due to too many requests in the queue.
         */
        int TooManyRequestsException = -105;
        /**
         * Ledger id overflow happens on ledger manager.
         *
         * @since 4.5
         */
        int LedgerIdOverflowException = -106;

        /**
         * Failure to serialize metadata.
         *
         * @since 4.9
         */
        int MetadataSerializationException = -107;

        /**
         * Operations failed due to ledger data in an unknown state.
         */
        int DataUnknownException = -108;

        /**
         * Generic exception code used to propagate in replication pipeline.
         */
        int ReplicationException = -200;

        /**
         * Unexpected condition.
         */
        int UnexpectedConditionException = -999;
    }

    /**
     * Code log message pool.
     */
    private static class LogMessagePool {
        private final int minCode;
        private final String[] pool;

        private LogMessagePool() {
            Field[] fields = Code.class.getDeclaredFields();
            this.minCode = minCode(fields);
            this.pool = new String[-minCode + 2]; // UnexpectedConditionException is an outlier
            initPoolMessages(fields);
        }

        private int minCode(Field[] fields) {
            int min = 0;
            for (Field field : fields) {
                int code = getFieldInt(field);
                if (code < min && code > Code.UnexpectedConditionException) {
                    min = code;
                }
            }
            return min;
        }

        private void initPoolMessages(Field[] fields) {
            for (Field field : fields) {
                int code = getFieldInt(field);
                int index = poolIndex(code);
                if (index >= 0) {
                    pool[index] = String.format("%s: %s", field.getName(), getMessage(code));
                }
            }
        }

        private static int getFieldInt(Field field) {
            try {
                return field.getInt(null);
            } catch (IllegalAccessException e) {
                return -1;
            }
        }

        private Object get(int code) {
            int index = poolIndex(code);
            String logMessage = index >= 0 ? pool[index] : null;
            return logMessage != null ? logMessage : new UnrecognizedCodeLogFormatter(code);
        }

        private int poolIndex(int code) {
            switch (code) {
            case Code.UnexpectedConditionException:
                return -minCode + 1;
            default:
                return code <= 0 && code >= minCode ? -minCode + code : -1;
            }
        }

        /**
         * Unrecognized code lazy log message formatter.
         */
        private static class UnrecognizedCodeLogFormatter {
            private final int code;

            private UnrecognizedCodeLogFormatter(int code) {
                this.code = code;
            }

            @Override
            public String toString() {
                return String.format("%d: %s", code, getMessage(code));
            }
        }
    }
}
