package de.duehl.basics.io.lock;

/*
 * Copyright 2021 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.OverlappingFileLockException;

import de.duehl.basics.debug.DebugHelper;
import de.duehl.basics.io.exceptions.FileNotFoundRuntimeException;
import de.duehl.basics.io.exceptions.IORuntimeException;

/**
 * Diese Klasse lockt eine Datei und gibt den Lock wieder frei.
 *
 * Siehe auch http://vafer.org/blog/20060806175846/
 *
 * https://examples.javacodegeeks.com/core-java/nio/channels/filelock-channels/
 *         java-nio-channels-filelock-example/
 *
 * @version 1.01     2021-02-09
 * @author Christian Dühl
 */

public class FileLock extends LockBase {

    private static final boolean DEBUG = false;

    /** Rechte, mit denen die Datei gelockt werden soll. */
    private final String rights;

    /** Interner Lock der Datei. */
    private java.nio.channels.FileLock lock;

    /**
     * Das zum Dateinamen und den Rechten geöffnete RandomAccessFile. Da man das Lock nicht ohne
     * den Channel des RandomAccessFile freigeben kann, muss dieses aufbewahrt werden.
     */
    private RandomAccessFile file;

    /**
     * Konstruktor der das Lesen und Schreiben lockt (Rechte "rw").
     *
     * @param filename
     *            Dateiname
     */
    public FileLock(String filename) {
        this(filename, "rw");
    }

    /**
     * Konstruktor.
     *
     * @param filename
     *            Dateiname
     * @param rights
     *            Rechte für das Lock:                                                     <ul><li>
     *            "r"   Open for reading only.                                            </li><li>
     *            "rw"  Open for reading and writing.                                     </li><li>
     *            "rws" Open for reading and writing, as with "rw", and
     *                  also require that every update to the file's content
     *                  or metadata be written synchronously to the underlying
     *                  storage device.                                                   </li><li>
     *            "rwd" Open for reading and writing, as with "rw", and also
     *                  require that every update to the file's content be written
     *                  synchronously to the underlying storage device.                  </li></ul>
     */
    public FileLock(String filename, String rights) {
        super(filename);
        this.rights = rights;
        lock = null;
        file = null;
    }

    /**
     * Versucht einen Lock auf die Datei zu erhalten.
     *
     * @return Erfolg
     * @throws FileNotFoundRuntimeException
     *             Wenn die Datei nicht gefunden wurde.
     */
    @Override
    public boolean lock() {
        checkInternalStateLock();
        if (null != lock) {
            return false;
        }

        say("Versuche Lock auf " + filename + " zu erhalten ...");
        boolean success = lockInternal();

        if (success) {
            say("Lock auf " + filename + " erhalten.");
            say("Lock is shared: " + lock.isShared());
        }
        else {
            say("Kein Lock erhalten, lock = " + lock);
            lock = null;
        }

        say("lock null ? " + (lock == null));
        say("file null ? " + (file == null));
        return success;
    }

    private void checkInternalStateLock() {
        boolean fileIsNull = file == null;
        boolean lockIsNull = lock == null;
        if (fileIsNull != lockIsNull) {
            throw new RuntimeException("Interner Fehler. fileIsNull = " + fileIsNull
                    + ", lockIsNull = " + lockIsNull);
        }
    }

    /**
     * Versucht einen Lock auf die Datei zu erhalten.
     *
     * @return Erfolg
     * @throws FileNotFoundRuntimeException
     *             Wenn die Datei nicht gefunden wurde.
     */
    private boolean lockInternal() {
        file = openRandomAccessFile();
        FileChannel channel = file.getChannel();

        say("Versuch den exclusiven Lock zu erhalten...");
        tryToLock(channel);

        if (null == lock) {
            say("Lock kann nicht erhalten werden. lock == null.");
            closeFile();
            return false;
        }

        return true;
    }

    /**
     * Erstellt ein RandomAccessFile zum Dateinamen und mit den passenden Rechten.
     *
     * @return RandomAccessFile
     * @throws FileNotFoundRuntimeException
     *             Wenn die Datei nicht gefunden wurde.
     */
    private RandomAccessFile openRandomAccessFile() {
        try {
            return new RandomAccessFile(filename, rights);
        }
        catch (FileNotFoundException exception) {
            throw new FileNotFoundRuntimeException(exception);
        }
    }

    /**
     * Versucht einen Lock auf die Datei zu erhalten.
     *
     * @param channel
     *            FileChannel auf dem versucht wird, den exklusiven Lock zu erhalten.
     * @return Erfolg. Der eigentliche lock wird in der Klassenvariable lock hinterlegt.
     */
    private boolean tryToLock(FileChannel channel) {
        try {
            lock = channel.tryLock(); // gets an exclusive lock
        }
        catch (IOException exception) {
            say("Lock kann nicht erhalten werden. IOException.");
            return false;
        }
        catch (OverlappingFileLockException exception) {
            say("Lock kann nicht erhalten werden. OverlappingFileLockException.");
            return false;
        }

        return true;
    }

    /**
     * Versucht den vorhandenen Lock wieder zu lösen.
     *
     * @return Erfolg.
     */
    @Override
    public boolean unlock() {
        checkInternalStateLock();
        if (null == lock) {
            return false;
        }

        say("Versuche Lock auf " + filename + " zu lösen ...");

        boolean success = tryToUnlock();

        if (success) {
            say("Lock auf " + filename + " gelöst.");
        }

        lock = null;
        closeFile();

        say("lock null ? " + (lock == null));
        say("file null ? " + (file == null));

        return success;
    }

    /** Versucht den Lock freizugeben. */
    private boolean tryToUnlock() {
        try {
            lock.release();
        }
        catch (IOException exception) {
            say("Lösen war nicht erfolgreich: " + exception.getMessage());
            exception.printStackTrace();
            return false;
        }

        return true;
    }

    /** Schließt die Datei und setzt file auf null. */
    private void closeFile() {
        try {
            file.close();
        }
        catch (IOException exception) {
            throw new IORuntimeException(exception);
        }
        file = null;
    }

    private void say(String text) {
        if (DEBUG) {
            DebugHelper.sayWithClassAndMethodAndTime(text);
        }
    }

}
