/*
 *  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
 *
 *      https://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.ivy.plugins.lock;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.ivy.util.Message;

public abstract class FileBasedLockStrategy extends AbstractLockStrategy {
    private static final int SLEEP_TIME = 100;

    private static final long DEFAULT_TIMEOUT = 2 * 60 * 1000;

    /**
     * The locker to use to make file lock attempts.
     */
    private FileLocker locker;

    private long timeout = DEFAULT_TIMEOUT;

    /**
     * Lock counter list must be static: locks are implicitly shared to the entire process, so the
     * list too much be.
     */
    private static final ConcurrentMap<File, ConcurrentMap<Thread, Integer>> currentLockHolders = new ConcurrentHashMap<>();

    protected FileBasedLockStrategy() {
        this(new CreateFileLocker(false), false);
    }

    protected FileBasedLockStrategy(boolean debugLocking) {
        this(new CreateFileLocker(debugLocking), debugLocking);
    }

    protected FileBasedLockStrategy(FileLocker locker, boolean debugLocking) {
        super(debugLocking);
        this.locker = locker;
    }

    protected boolean acquireLock(File file) throws InterruptedException {
        Thread currentThread = Thread.currentThread();
        if (isDebugLocking()) {
            debugLocking("acquiring lock on " + file);
        }
        long start = System.currentTimeMillis();
        do {
            synchronized (currentLockHolders) {
                if (isDebugLocking()) {
                    debugLocking("entered synchronized area (locking)");
                }
                int lockCount = hasLock(file, currentThread);
                if (isDebugLocking()) {
                    debugLocking("current status for " + file + " is " + lockCount
                            + " held locks: " + getCurrentLockHolderNames(file));
                }
                if (lockCount < 0) {
                    /* Another thread in this process holds the lock; we need to wait */
                    if (isDebugLocking()) {
                        debugLocking("waiting for another thread to release the lock: "
                                + getCurrentLockHolderNames(file));
                    }
                } else if (lockCount > 0) {
                    int holdLocks = incrementLock(file, currentThread);
                    if (isDebugLocking()) {
                        debugLocking("reentrant lock acquired on " + file + " in "
                                + (System.currentTimeMillis() - start) + "ms" + " - hold locks = "
                                + holdLocks);
                    }
                    return true;
                } else {
                    /* No prior lock on this file is held at all */
                    if (locker.tryLock(file)) {
                        if (isDebugLocking()) {
                            debugLocking("lock acquired on " + file + " in "
                                    + (System.currentTimeMillis() - start) + "ms");
                        }
                        incrementLock(file, currentThread);
                        return true;
                    }
                }
            }
            if (isDebugLocking()) {
                debugLocking("failed to acquire lock; sleeping for retry...");
            }
            Thread.sleep(SLEEP_TIME);
        } while (System.currentTimeMillis() - start < timeout);
        return false;
    }

    protected void releaseLock(File file) {
        Thread currentThread = Thread.currentThread();
        if (isDebugLocking()) {
            debugLocking("releasing lock on " + file);
        }
        synchronized (currentLockHolders) {
            if (isDebugLocking()) {
                debugLocking("entered synchronized area (unlocking)");
            }
            int holdLocks = decrementLock(file, currentThread);
            if (holdLocks == 0) {
                locker.unlock(file);
                if (isDebugLocking()) {
                    debugLocking("lock released on " + file);
                }
            } else {
                if (isDebugLocking()) {
                    debugLocking("reentrant lock released on " + file + " - hold locks = "
                            + holdLocks);
                }
            }
        }
    }

    private static void debugLocking(String msg) {
        Message.info(Thread.currentThread() + " " + System.currentTimeMillis() + " " + msg);
    }

    /**
     * Determine the state of the lockfile.
     *
     * Must be called from within a synchronized block.
     *
     * Three possibilities exist: - The lock is held by the current thread (>0) - The lock is held
     * by one or more different threads (-1) - The lock is not held at all (0).
     *
     * @param file
     *            file to lock
     * @param forThread
     *            thread for which lock status is being queried
     */
    private int hasLock(File file, Thread forThread) {
        ConcurrentMap<Thread, Integer> locksPerThread = currentLockHolders.get(file);
        if (locksPerThread == null) {
            return 0;
        }
        if (locksPerThread.isEmpty()) {
            return 0;
        }
        Integer counterObj = locksPerThread.get(forThread);
        int counter = (counterObj == null) ? 0 : counterObj;
        if (counter > 0) {
            return counter;
        } else {
            return -1;
        }
    }

    /**
     * Record that this thread holds the lock.
     *
     * Asserts that the lock has been previously grabbed by this thread. Must be called from a
     * synchronized block in which the lock was grabbed.
     *
     * @param file
     *            file which has been locked
     * @param forThread
     *            thread for which locking occurred
     * @return number of times this thread has grabbed the lock
     */
    private int incrementLock(File file, Thread forThread) {
        ConcurrentMap<Thread, Integer> locksPerThread = currentLockHolders.get(file);
        if (locksPerThread == null) {
            locksPerThread = new ConcurrentHashMap<>();
            currentLockHolders.put(file, locksPerThread);
        }
        Integer c = locksPerThread.get(forThread);
        int holdLocks = (c == null) ? 1 : c + 1;
        locksPerThread.put(forThread, holdLocks);
        return holdLocks;
    }

    /**
     * Decrease depth of this thread's lock.
     *
     * Must be called within a synchronized block.
     *
     * If this returns 0, the caller is responsible for releasing the lock within that same block.
     *
     * @param file
     *            file for which lock depth is being decreased
     * @param forThread
     *            thread for which lock depth is being decreased
     * @return remaining depth of this lock
     */
    private int decrementLock(File file, Thread forThread) {
        ConcurrentMap<Thread, Integer> locksPerThread = currentLockHolders.get(file);
        if (locksPerThread == null) {
            throw new RuntimeException("Calling decrementLock on a thread which holds no locks");
        }
        Integer c = locksPerThread.get(forThread);
        int oldHeldLocks = (c == null) ? 0 : c;
        if (oldHeldLocks <= 0) {
            throw new RuntimeException("Calling decrementLock on a thread which holds no locks");
        }
        int newHeldLocks = oldHeldLocks - 1;
        if (newHeldLocks > 0) {
            locksPerThread.put(forThread, newHeldLocks);
        } else {
            locksPerThread.remove(forThread);
        }
        return newHeldLocks;
    }

    /**
     * Return a string naming the threads which currently hold this lock.
     *
     * @param file File
     * @return String
     */
    protected String getCurrentLockHolderNames(File file) {
        StringBuilder sb = new StringBuilder();
        ConcurrentMap<Thread, Integer> m = currentLockHolders.get(file);
        if (m == null) {
            return "(NULL)";
        }
        for (Thread t : m.keySet()) {
            if (sb.length() > 0) {
                sb.append(", ");
            }
            sb.append(t.toString());
        }
        return sb.toString();
    }

    public interface FileLocker {
        boolean tryLock(File f);

        void unlock(File f);
    }

    /**
     * "locks" a file by creating it if it doesn't exist, relying on the
     * {@link File#createNewFile()} atomicity.
     */
    public static class CreateFileLocker implements FileLocker {
        private boolean debugLocking;

        public CreateFileLocker(boolean debugLocking) {
            this.debugLocking = debugLocking;
        }

        public boolean tryLock(File file) {
            try {
                if (file.getParentFile().exists() || file.getParentFile().mkdirs()) {
                    if (file.createNewFile()) {
                        DeleteOnExitHook.add(file);
                        return true;
                    } else {
                        if (debugLocking) {
                            debugLocking("file creation failed " + file);
                        }
                    }
                }
            } catch (IOException e) {
                // ignored
                Message.verbose("file creation failed due to an exception: " + e.getMessage()
                        + " (" + file + ")");
            }
            return false;
        }

        public void unlock(File file) {
            file.delete();
            DeleteOnExitHook.remove(file);
        }
    }

    /**
     * Locks a file using the {@link FileLock} mechanism.
     */
    public static class NIOFileLocker implements FileLocker {

        private ConcurrentMap<File, LockData> locks = new ConcurrentHashMap<>();

        private boolean debugLocking;

        public NIOFileLocker(boolean debugLocking) {
            this.debugLocking = debugLocking;
        }

        private static class LockData {
            private RandomAccessFile raf;

            private FileLock l;

            LockData(RandomAccessFile raf, FileLock l) {
                this.raf = raf;
                this.l = l;
            }
        }

        @SuppressWarnings("resource")
        public boolean tryLock(File file) {
            try {
                if (file.getParentFile().exists() || file.getParentFile().mkdirs()) {
                    // this must not be closed until unlock
                    RandomAccessFile raf = new RandomAccessFile(file, "rw");
                    FileLock l = raf.getChannel().tryLock();
                    if (l != null) {
                        synchronized (this) {
                            locks.put(file, new LockData(raf, l));
                        }
                        return true;
                    } else {
                        if (debugLocking) {
                            debugLocking("failed to acquire lock on " + file);
                        }
                    }
                }
            } catch (IOException e) {
                // ignored
                Message.verbose("file lock failed due to an exception: " + e.getMessage() + " ("
                        + file + ")");
            }
            return false;
        }

        public void unlock(File file) {
            synchronized (this) {
                LockData data = locks.get(file);
                if (data == null) {
                    throw new IllegalArgumentException("file not previously locked: " + file);
                }

                try {
                    locks.remove(file);
                    data.l.release();
                    data.raf.close();
                } catch (IOException e) {
                    Message.error("problem while releasing lock on " + file + ": " + e.getMessage());
                }
            }
        }

    }
}
