/*
 * 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
 *
 *    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.gobblin.metastore.testing;

import java.io.Closeable;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigRenderOptions;


public class TestMetastoreDatabaseFactory {
    private static final Object syncObject = new Object();
    private static TestMetastoreDatabaseServer testMetastoreDatabaseServer;
    private static Set<ITestMetastoreDatabase> instances = new HashSet<>();

    private TestMetastoreDatabaseFactory() {
    }

    public static Config getDefaultConfig() {
      return ConfigFactory.defaultOverrides().withFallback(ConfigFactory.load());
    }

    public static ITestMetastoreDatabase get() throws Exception {
        return TestMetastoreDatabaseFactory.get("latest");
    }

    public static ITestMetastoreDatabase get(String version) throws Exception {
        return get(version, getDefaultConfig());
    }

    public static ITestMetastoreDatabase get(String version, Config dbConfig) throws Exception {
        try {
            synchronized (syncObject) {
                ensureDatabaseServerExists(dbConfig);
                TestMetadataDatabase instance = new TestMetadataDatabase(testMetastoreDatabaseServer, version);
                instances.add(instance);
                return instance;
            }
        }
        catch (Exception e) {
            throw new RuntimeException("Failed to create TestMetastoreDatabase with version " + version +
               " and config " + dbConfig.root().render(ConfigRenderOptions.defaults().setFormatted(true).setJson(true))
               + " cause: " + e, e);
        }

    }

    static void release(ITestMetastoreDatabase instance) throws IOException {
        synchronized (syncObject) {
            if (instances.remove(instance) && instances.isEmpty()) {
                testMetastoreDatabaseServer.close();
                testMetastoreDatabaseServer = null;
            }
        }
    }

    private static void ensureDatabaseServerExists(Config dbConfig) throws Exception {
        if (testMetastoreDatabaseServer == null) {
            try (Mutex ignored = new Mutex()) {
                if (testMetastoreDatabaseServer == null) {
                    testMetastoreDatabaseServer = new TestMetastoreDatabaseServer(dbConfig);
                }
            }
        }
    }

    private static class Mutex implements Closeable {
        private final Object syncObject = new Object();
        private final AtomicBoolean isLocked = new AtomicBoolean(false);
        private FileChannel fileChannel;
        private FileLock fileLock;

        public Mutex() throws IOException {
            take();
        }

        @Override
        public void close() {
            release();
        }

        private boolean take() throws IOException {
            if (!isLocked.get()) {
                synchronized (syncObject) {
                    if (!isLocked.get()) {
                        if (fileChannel == null) {
                            Path lockPath = Paths.get(System.getProperty("user.home")).resolve(".embedmysql.lock");
                            fileChannel = FileChannel.open(lockPath, StandardOpenOption.CREATE,
                                    StandardOpenOption.WRITE, StandardOpenOption.READ);
                        }
                        fileLock = fileChannel.lock();
                        isLocked.set(true);
                        return true;
                    }
                    return true;
                }
            }
            return true;
        }

        private boolean release() {
            if (isLocked.get()) {
                synchronized (syncObject) {
                    if (isLocked.get()) {
                        if (fileLock != null) {
                            boolean result = true;
                            try {
                                fileLock.close();
                                fileLock = null;
                                isLocked.set(false);
                            } catch (IOException ignored) {
                                result = false;
                            }
                            if (fileChannel != null) {
                                try {
                                    fileChannel.close();
                                } catch (IOException ignored) {
                                    result = false;
                                }
                            }
                            return result;
                        }
                        isLocked.set(false);
                        return true;
                    }
                    return true;
                }
            }
            return true;
        }
    }
}

