package de.renew.util;

import java.io.IOException;
import java.io.NotSerializableException;
import java.io.PrintWriter;
import java.io.Serial;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;


/**
 * In essence this class duplicates the functionality of the
 * synchronized statement. But locks can be used without
 * a textual matching in the source code, which allows
 * more flexible (and hence more dangerous) synchronization
 * schemes.
 **/
public class Lock implements Serializable {
    /**
     * The cross-instance static logger entity for this class, logging all activity
     */
    public final static Logger logger = Logger.getLogger(Lock.class);
    /**
     * List of exceptions that documents how many lock operations without unlock operations had been executed.
     * The Throwables hold the stack trace at the time of documentation.
     */
    private transient List<Throwable> _lastLockTraces;
    /**
     * the Thread object currently holding this lock
     */
    private transient Thread _lockingThread;
    /**
     * holds the current number of lock operations without number of unlock operations.
     */
    private transient int _lockCount;

    /**
     * Produce a new Lock object with which to lock some ressources.
     */
    public Lock() {
        _lockingThread = null;
        _lockCount = 0;
        _lastLockTraces = new ArrayList<>();
    }

    /**
     * For the currently calling thread, lock the assigned resource, configure the lock and log debug information if enabled.
     * If the lock is already locked by another thread, send the caller to wait.
     */
    public synchronized void lock() {
        Thread currentThread = Thread.currentThread();
        while (_lockingThread != null && !mayLock(_lockingThread)) {
            try {
                if (logger.isDebugEnabled()) {
                    logger.debug(
                        new LockDebugLogEvent(
                            "Thread " + Thread.currentThread() + " has to wait for lock.", this,
                            _lockCount, _lastLockTraces, new Throwable("Waiting thread trace")));
                }
                wait();
            } catch (InterruptedException ignored) {
            }
        }

        _lockCount++;
        if (logger.isTraceEnabled()) {
            logger.trace("Locked " + this + " by Thread: " + currentThread);
        }
        if (_lockCount == 1) {
            _lockingThread = currentThread;
        }
        if (logger.isDebugEnabled()) {
            _lastLockTraces.add(new Throwable("Locking thread trace no. " + _lockCount));
        }
    }

    /**
     * If the resource is currently locked and the calling thread is the locking thread, unlock the resource
     * and notify any waiting threads that an unlock has occurred.
     */
    public synchronized void unlock() {
        verify();
        _lockCount--;
        if (logger.isTraceEnabled()) {
            logger.trace("Unlocked " + this + " by Thread: " + Thread.currentThread());
        }
        if (_lockCount == 0) {
            _lockingThread = null;
            _lastLockTraces.clear();
        }
        notify();
    }

    /**
     * Verifies that the thread calling this method is the one that has locked the resource and is allowed to
     * execute on it. If allowed, the method returns without a return value.
     * If the calling thread is not allowed, an {@link IllegalStateException} is thrown, documenting
     * the caller thread and the locking thread.
     */
    public synchronized void verify() {
        if (!mayLock(_lockingThread)) {
            if (_lockingThread == null) {
                throw new IllegalStateException("The lock is not locked.");
            } else {
                if (logger.isDebugEnabled()) {
                    logger.debug(
                        new LockDebugLogEvent(
                            "The lock was locked by another thread.", this, _lockCount,
                            _lastLockTraces, new Throwable("Wrong unlocking thread trace")));
                }
                throw new IllegalStateException(
                    "The lock was locked by another thread (" + _lockingThread + ",hc="
                        + _lockingThread.hashCode() + " instead of " + Thread.currentThread()
                        + ",hc=" + Thread.currentThread().hashCode() + ").");
            }
        }
    }

    /**
     * Determines whether the current thread is allowed to lock
     * this lock which is already assigned to <code>lockingThread</code>.
     * This method is intended to be overridden by subclasses to implement
     * different locking policies.
     *
     * @param lockingThread The thread that currently holds this lock.
     *
     * @return true if the current thread is allowed to re-acquire the lock, false
     * otherwise
     *
     */
    protected boolean mayLock(Thread lockingThread) {
        return Thread.currentThread() == lockingThread;
    }

    /**
     * Serialization method, behaves like default writeObject
     * method except checking additional error conditions.
     * Throws NotSerializableException if the lock is in use.
     * @param out the OutputStream to use for serialization
     * @throws IOException if I/O operations on the output stream fail
     **/
    @Serial
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        synchronized (this) {
            if ((_lockCount == 0) && (_lockingThread == null)) {
                out.defaultWriteObject();
            } else {
                throw new NotSerializableException("de.renew.util.Lock: " + this + " is in use.");
            }
        }
    }

    /**
     * Deserialization method, behaves like default readObject
     * method except restoring additional transient fields.
     * Resets lockCount and lockingThread to null.
     * @param in the InoutStream to use for deserialization
     * @throws IOException if the I/O on the input stream fails
     * @throws ClassNotFoundException if the class to deserialize to cannot be found
     **/
    @Serial
    private void readObject(java.io.ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        in.defaultReadObject();
        _lockCount = 0;
        _lockingThread = null;
        _lastLockTraces = new ArrayList<>();
    }

    /**
     * Encapsulating class that records events on the lock, such as the situation, the Lock object ID, that objects
     * number of lock operations without matching unlock operations, the history of these lock without unlock operations.
     *
     * @param situation      String describing what situation was logged
     * @param lock           The lock object from which this Event Log was generated
     * @param lockCount      the number of lock operations without matching unlock operations at the time of the log generation
     * @param lastLockTraces the historical state of the lockCount on the Lock object, and stack traces every time the count changed,
     *                       encapsulated as Throwables.
     * @param currentTrace   the type of the lock trace - waiting thread, unlocking thread, or other, encapsulated in a Throwable
     */
    private record LockDebugLogEvent(String situation, Lock lock, int lockCount,
        List<Throwable> lastLockTraces, Throwable currentTrace) {
        /**
         * Generate a new Debug Event Log
         *
         * @param situation      what situation has occurred prompting this event log
         * @param lock           the object in which this event is being logged
         * @param lockCount      the lockCount of the Lock object
         * @param lastLockTraces the historical trace of the lockCount with associated stack traces, encapsulated as Throwables
         * @param currentTrace   the situation in which the trace is being generated, encapsulated as Throwable
         */
        private LockDebugLogEvent(
            String situation, Lock lock, int lockCount, List<Throwable> lastLockTraces,
            Throwable currentTrace)
        {
            this.situation = situation;
            this.lock = lock;
            this.lockCount = lockCount;
            this.lastLockTraces = new ArrayList<>(lastLockTraces);
            this.currentTrace = currentTrace;
        }

        /**
         * Produce a string representation of this event log.
         *
         * @return The string representation
         */
        @Override
        public String toString() {
            StringWriter writer = new StringWriter();
            PrintWriter out = new PrintWriter(writer);

            out.println(situation);
            out.println("Lock: " + lock);
            out.println("Current lock count: " + lockCount);
            currentTrace.printStackTrace(out);
            for (Throwable trace : lastLockTraces) {
                if (trace != null) {
                    trace.printStackTrace(out);
                }
            }
            return writer.toString();
        }
    }
}