/*
 * Copyright (C) 2025  Tim Zimmermann <tim@aglais.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.aglais.zms.db.core.model;

import org.aglais.zms.util.Log;
import org.jetbrains.annotations.NotNull;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * A utility class designed to manage and track changes to a data object of type {@code T}.
 * The class provides methods to check if the data has been modified, reset the data to its
 * original state, and update the original state to reflect the current state.
 *
 * @param <T> The type of the data object being managed.
 */
public class ModifiedStateTracker<T> implements IModifiedStateTracker<T> {

    private static final String LOG_TAG = ModifiedStateTracker.class.getSimpleName();

    private final T data;
    private final T originalData;
    private final List<Field> fields;

    /**
     * Creates a new instance of the {@code ModifiedStateTracker} class, initializing the current
     * and original state of the data object being tracked.
     *
     * @param data The data object of type {@code T} to be managed by this tracker. Must not be null.
     */
    public ModifiedStateTracker(@NotNull T data) {
        this.data = data;
        this.fields = getAllFields(data.getClass());

        // Clone data
        try {
            @SuppressWarnings("unchecked")
            Constructor<T> constructor = (Constructor<T>) data.getClass().getDeclaredConstructor();
            originalData = constructor.newInstance();

            copyValues(data, originalData);
        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
                 IllegalAccessException e) {
            throw new IllegalArgumentException(data.getClass().getSimpleName()
                    + " must implement a public no-argument constructor", e);
        }
    }

    @Override
    public T getData() {
        return data;
    }

    /**
     * Retrieves all declared fields of the specified class and its superclasses.
     * This includes private, protected, package-private, and public fields.
     *
     * @param targetClass The class whose fields are to be retrieved
     * @return a list of all declared fields in the class hierarchy
     */
    private List<Field> getAllFields(Class<?> targetClass) {
        List<Field> fields = new ArrayList<>();

        while (targetClass != null) {
            for (Field field : targetClass.getDeclaredFields()) {
                // Ignore static fields
                if ((field.getModifiers() & Modifier.STATIC) == 0)
                    fields.add(field);
            }
            targetClass = targetClass.getSuperclass();
        }

        return fields;
    }

    @Override
    public boolean isModified() {
        for (Field field : fields) {
            field.setAccessible(true);

            try {
                if (!Objects.equals(field.get(data), field.get(originalData))) {
                    return true;
                }
            } catch (IllegalAccessException e) {
                Log.e(LOG_TAG, "Failed to access field " + field.getName() + " in "
                        + data.getClass().getSimpleName(), e);
            }
        }

        return false;
    }

    /**
     * Copies the values of declared fields from one object to another of the same type.
     *
     * @param src The source object from which to copy field values. Must not be null.
     * @param dst The destination object to which the field values are copied. Must not be null.
     */
    private void copyValues(T src, T dst) {
        for (Field field : fields) {
            field.setAccessible(true);
            try {
                field.set(dst, field.get(src));
            } catch (IllegalAccessException e) {
                Log.e(LOG_TAG, "Failed to access field " + field.getName() + " in "
                        + src.getClass().getSimpleName(), e);
            }
        }
    }

    @Override
    public void applyChanges() {
        copyValues(data, originalData);
    }

    @Override
    public void discardChanges() {
        copyValues(originalData, data);
    }
}
