/* Copyright 2017 Google Inc.
 *
 * 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.

 *  2019.12.15-Changed constructor HuaweiApp
 *  2019.12.15-Changed method initializeApp
 *                  Huawei Technologies Co., Ltd.
 *
 */
package com.wecloud.im.push.huawei.messaging;

import com.google.common.collect.ImmutableList;
import com.wecloud.im.push.huawei.util.ValidatorUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * The entry point of Huawei Java SDK.
 * <p>{@link HuaweiApp#initializeApp(HuaweiOption)} initializes the app instance.
 */
public class HuaweiApp {
    private static final Logger logger = LoggerFactory.getLogger(HuaweiApp.class);

    private String appId;
    private HuaweiOption option;

    /** Global lock */
    private static final Object appsLock = new Object();

    /** Store a map of <appId, HuaweiApp> */
    private static final Map<String, HuaweiApp> instances = new HashMap<>();

    /** HuaweiMessaging can be added in the pattern of service, whcih is designed for Scalability */
    private final Map<String, HuaweiService> services = new HashMap<>();

    private TokenRefresher tokenRefresher;

    private volatile ScheduledExecutorService scheduledExecutor;

    private ThreadManager threadManager;

    private ThreadManager.HuaweiExecutors executors;

    private final AtomicBoolean deleted = new AtomicBoolean();

    /** lock for synchronizing all internal HuaweiApp state changes */
    private final Object lock = new Object();

    private HuaweiApp(HuaweiOption option) {
        ValidatorUtils.checkArgument(option != null, "HuaweiOption must not be null");
        this.option = option;
        this.appId = option.getCredential().getAppId();
        this.tokenRefresher = new TokenRefresher(this);
        this.threadManager = option.getThreadManager();
        this.executors = threadManager.getHuaweiExecutors(this);
    }

    public HuaweiOption getOption() {
        return option;
    }

    public String getAppId() {
        return appId;
    }

    /**
     * Returns the instance identified by the unique appId, or throws if it does not exist.
     *
     * @param appId represents the id of the {@link HuaweiApp} instance.
     * @return the {@link HuaweiApp} corresponding to the id.
     * @throws IllegalStateException if the {@link HuaweiApp} was not initialized, either via {@link
     *     #initializeApp(HuaweiOption)} or {@link #getApps()}.
     */
    public static HuaweiApp getInstance(HuaweiOption option) {
        String appId = option.getCredential().getAppId();
        synchronized (appsLock) {
            HuaweiApp app = instances.get(appId);
            if (app != null) {
                return app;
            }
//            String errorMessage = MessageFormat.format("HuaweiApp with id {0} doesn't exist", appId);
//            throw new IllegalStateException(errorMessage);
            return initializeApp(option);
        }
    }

    /**
     * Initializes the {@link HuaweiApp} instance using the given option.
     *
     * @throws IllegalStateException if the app instance has already been initialized.
     */
    public static HuaweiApp initializeApp(HuaweiOption option) {
        String appId = option.getCredential().getAppId();
        final HuaweiApp app;
        synchronized (appsLock) {
            if (!instances.containsKey(appId)) {
                ValidatorUtils.checkState(!instances.containsKey(appId), "HuaweiApp with id " + appId + " already exists!");
                app = new HuaweiApp(option);
                instances.put(appId, app);
                app.startTokenRefresher();
            } else {
                app = getInstance(option);
            }

        }
        return app;
    }

    /** Returns a list of all HuaweiApps. */
    public static List<HuaweiApp> getApps() {
        synchronized (appsLock) {
            return ImmutableList.copyOf(instances.values());
        }
    }

    /**
     * Get all appIds which are be stored in the instances
     */
    public static List<String> getAllAppIds() {
        Set<String> allAppIds = new HashSet<>();
        synchronized (appsLock) {
            for (HuaweiApp app : instances.values()) {
                allAppIds.add(app.getAppId());
            }
        }
        List<String> sortedIdList = new ArrayList<>(allAppIds);
        Collections.sort(sortedIdList);
        return sortedIdList;
    }

    /**
     * Deletes the {@link HuaweiApp} and all its data. All calls to this {@link HuaweiApp}
     * instance will throw once it has been called.
     *
     * <p>A no-op if delete was called before.
     */
    public void delete() {
        synchronized (lock) {
            boolean valueChanged = deleted.compareAndSet(false /* expected */, true);
            if (!valueChanged) {
                return;
            }

            try {
                this.getOption().getHttpClient().close();
                this.getOption().getCredential().getHttpClient().close();
            } catch (IOException e) {
                logger.debug("Fail to close httpClient");
            }

            for (HuaweiService service : services.values()) {
                service.destroy();
            }
            services.clear();
            tokenRefresher.stop();

            threadManager.releaseHuaweiExecutors(this, executors);
            if (scheduledExecutor != null) {
                scheduledExecutor.shutdown();
                scheduledExecutor = null;
            }
        }

        synchronized (appsLock) {
            instances.remove(this.getAppId());
        }
    }

    /**
     * Check the app is not deleted, whcic is the premisi of some methods
     */
    private void checkNotDeleted() {
        String errorMessage = MessageFormat.format("HuaweiApp with id {0} was deleted", getAppId());
        ValidatorUtils.checkState(!deleted.get(), errorMessage);
    }

    /**
     * Singleton mode, ensure the scheduleExecutor is singleton
     */
    private ScheduledExecutorService singleScheduledExecutorService() {
        if (scheduledExecutor == null) {
            synchronized (lock) {
                checkNotDeleted();
                if (scheduledExecutor == null) {
                    scheduledExecutor = new HuaweiScheduledExecutor(getThreadFactory(), "huawei-scheduled-worker");
                }
            }
        }
        return scheduledExecutor;
    }

    public ThreadFactory getThreadFactory() {
        return threadManager.getThreadFactory();
    }

    private ScheduledExecutorService getScheduledExecutorService() {
        return singleScheduledExecutorService();
    }

    ScheduledFuture<?> schedule(Runnable runnable, long initialDelay, long period) {
        return getScheduledExecutorService().scheduleWithFixedDelay(runnable, initialDelay, period, TimeUnit.MILLISECONDS);
    }

    /**
     * Add service to the app, such as HuaweiMessaging, other services can be added if needed
     */
    void addService(HuaweiService service) {
        synchronized (lock) {
            checkNotDeleted();
            ValidatorUtils.checkArgument(!services.containsKey(service.getId()), "service already exists");
            services.put(service.getId(), service);
        }
    }

    HuaweiService getService(String id) {
        synchronized (lock) {
            return services.get(id);
        }
    }

    /**
     * Start the scheduled task for refreshing token automatically
     */
    public void startTokenRefresher() {
        synchronized (lock) {
            checkNotDeleted();
            tokenRefresher.start();
        }
    }

    /** It is just for test */
    public static void clearInstancesForTest() {
        synchronized (appsLock) {
            //copy before delete
            for (HuaweiApp app : ImmutableList.copyOf(instances.values())) {
                app.delete();
            }
            instances.clear();
        }
    }
}
