/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * 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.
 */

package com.android.server.devicelock;

import android.annotation.WorkerThread;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.Xml;

import com.android.devicelockcontroller.util.ThreadAsserts;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.Executor;

/**
 * Class that manages persisting device state data for the system service.
 */
public final class DeviceLockPersistentStore {
    private static final String TAG = DeviceLockPersistentStore.class.getSimpleName();
    private static final String SYSTEM_DIR = "system";
    private static final String DEVICE_LOCK_DIR = "device_lock";
    private static final String DEVICE_STATE_FILE = "device_state.xml";
    private static final String TAG_DEVICE_STATE = "device_state";
    private static final String ATTR_IS_DEVICE_FINALIZED = "is_device_finalized";

    private final Executor mBgExecutor;
    private final File mFile;

    DeviceLockPersistentStore(Executor bgExecutor, File dataDirectory) {
        mBgExecutor = bgExecutor;
        final File systemDir = new File(dataDirectory, SYSTEM_DIR);
        final File deviceLockDir = new File(systemDir, DEVICE_LOCK_DIR);
        if (!deviceLockDir.exists()) {
            final boolean madeDirs = deviceLockDir.mkdirs();
            if (!madeDirs) {
                Slog.e(TAG, "Failed to make directory " + deviceLockDir.getAbsolutePath());
            }
        }
        mFile = new File(deviceLockDir, DEVICE_STATE_FILE);
    }

    /**
     * Schedule a write of the finalized state.
     *
     * @param finalized true if device is fully finalized
     */
    public void scheduleWrite(boolean finalized) {
        mBgExecutor.execute(() -> writeState(finalized));
    }

    /**
     * Read the finalized state from disk
     *
     * @param callback callback for when state is read
     * @param callbackExecutor executor to run callback on
     */
    public void readFinalizedState(DeviceStateCallback callback, Executor callbackExecutor) {
        mBgExecutor.execute(() -> {
            final boolean isFinalized = readState();
            callbackExecutor.execute(() -> callback.onDeviceStateRead(isFinalized));
        });
    }

    @WorkerThread
    private void writeState(boolean finalized) {
        ThreadAsserts.assertWorkerThread("writeState");
        synchronized (this) {
            AtomicFile atomicFile = new AtomicFile(mFile);

            try (FileOutputStream fileOutputStream = atomicFile.startWrite()) {
                try {
                    XmlSerializer serializer = Xml.newSerializer();
                    serializer.setOutput(fileOutputStream, Xml.Encoding.UTF_16.name());
                    serializer.startDocument(Xml.Encoding.UTF_16.name(), true /* standalone */);
                    writeToXml(serializer, finalized);
                    serializer.endDocument();
                    fileOutputStream.flush();
                    atomicFile.finishWrite(fileOutputStream);
                } catch (IOException e) {
                    Slog.e(TAG, "Failed to write to XML", e);
                    atomicFile.failWrite(fileOutputStream);
                }
            } catch (IOException e) {
                Slog.e(TAG, "Failed to start write", e);
            }
        }
    }

    private void writeToXml(XmlSerializer serializer, boolean finalized) throws IOException {
        serializer.startTag(null /* namespace */, TAG_DEVICE_STATE);
        serializer.attribute(null /* namespace */,
                ATTR_IS_DEVICE_FINALIZED, Boolean.toString(finalized));
        serializer.endTag(null /* namespace */, TAG_DEVICE_STATE);
    }

    @WorkerThread
    private boolean readState() {
        ThreadAsserts.assertWorkerThread("readState");
        synchronized (this) {
            if (!mFile.exists()) {
                return false;
            }
            AtomicFile atomicFile = new AtomicFile(mFile);

            try (FileInputStream inputStream = atomicFile.openRead()) {
                XmlPullParser parser = Xml.newPullParser();
                parser.setInput(inputStream, /* inputEncoding= */ null);
                return getStateFromXml(parser);
            } catch (XmlPullParserException | IOException e) {
                Slog.e(TAG, "Failed to read XML", e);
                return false;
            }
        }
    }

    private boolean getStateFromXml(XmlPullParser parser)
            throws XmlPullParserException, IOException {
        while (parser.getEventType() != XmlPullParser.START_TAG
                || !TAG_DEVICE_STATE.equals(parser.getName())) {
            if (parser.getEventType() == XmlPullParser.END_DOCUMENT) {
                throw new XmlPullParserException("Malformed XML. Unable to find start of tag.");
            }
            parser.next();
        }
        return Boolean.parseBoolean(
                parser.getAttributeValue(null /* namespace */, ATTR_IS_DEVICE_FINALIZED));
    }

    /**
     * Callback for when state is read from disk.
     */
    interface DeviceStateCallback {
        /**
         * Callback for when state is finished reading from disk.
         *
         * @param isFinalized whether device is finalized
         */
        void onDeviceStateRead(boolean isFinalized);
    }
}
