package ctbrec.recorder.postprocessing;

import static ctbrec.StringUtil.*;
import static java.util.Optional.*;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;

import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording;
import ctbrec.StringUtil;
import ctbrec.sites.Site;

public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPostProcessor {

    public String fillInPlaceHolders(String input, PostProcessingContext ctx) {
        Recording rec = ctx.getRecording();
        Config config = ctx.getConfig();
        Optional<ModelGroup> modelGroup = Optional.ofNullable(ctx.getRecorder()).flatMap(r -> r.getModelGroup(rec.getModel()));

        Map<String, Function<String, Optional<String>>> placeholderValueSuppliers = new HashMap<>();
        placeholderValueSuppliers.put("modelName", r -> ofNullable(rec.getModel().getName()));
        placeholderValueSuppliers.put("modelDisplayName", r -> ofNullable(rec.getModel().getDisplayName()));
        placeholderValueSuppliers.put("modelSanitizedName", r -> getSanitizedName(rec.getModel()));
        placeholderValueSuppliers.put("siteName", r -> ofNullable(rec.getModel().getSite()).map(Site::getName));
        placeholderValueSuppliers.put("siteSanitizedName", r -> getSanitizedSiteName(rec));
        placeholderValueSuppliers.put("fileSuffix", r -> getFileSuffix(rec));
        placeholderValueSuppliers.put("epochSecond", r -> ofNullable(rec.getStartDate()).map(Instant::getEpochSecond).map(l -> Long.toString(l))); // NOSONAR
        placeholderValueSuppliers.put("modelNotes", r -> getSanitizedModelNotes(config, rec.getModel()));
        placeholderValueSuppliers.put("recordingNotes", r -> getSanitizedRecordingNotes(rec));
        placeholderValueSuppliers.put("recordingsDir", r -> Optional.of(config.getSettings().recordingsDir));
        placeholderValueSuppliers.put("absolutePath", r -> Optional.of(rec.getPostProcessedFile().getAbsolutePath()));
        placeholderValueSuppliers.put("absoluteParentPath", r -> Optional.of(rec.getPostProcessedFile().getParentFile().getAbsolutePath()));
        placeholderValueSuppliers.put("modelGroupName", r -> modelGroup.map(ModelGroup::getName));
        placeholderValueSuppliers.put("modelGroupId", r -> modelGroup.map(ModelGroup::getId).map(UUID::toString));
        placeholderValueSuppliers.put("utcDateTime", pattern -> replaceUtcDateTime(rec, pattern));
        placeholderValueSuppliers.put("localDateTime", pattern -> replaceLocalDateTime(rec, pattern));

        String output = fillInPlaceHolders(input, placeholderValueSuppliers);
        return output;
    }

    private Optional<String> getSanitizedName(Model model) {
        String name = model.getSanitizedNamed();
        if (StringUtil.isBlank(name)) {
            return Optional.empty();
        } else {
            return Optional.of(name);
        }
    }

    private String fillInPlaceHolders(String input, Map<String, Function<String, Optional<String>>> placeholderValueSuppliers) {
        boolean somethingReplaced = false;
        do {
            somethingReplaced = false;
            int end = input.indexOf("}");
            if (end > 0) {
                int start = input.substring(0, end).lastIndexOf("${");
                if (start >= 0) {
                    String placeholder = input.substring(start, end + 1);
                    String placeholderName = placeholder.substring(2, placeholder.length() - 1);
                    String defaultValue = null;
                    String expression = null;
                    int questionMark = placeholder.indexOf('?');
                    if (questionMark > 0) {
                        placeholderName = placeholder.substring(2, questionMark);
                        defaultValue = placeholder.substring(questionMark + 1, placeholder.length() - 1);
                    } else {
                        defaultValue = "";
                    }
                    int bracket = placeholder.indexOf('(');
                    if (bracket > 0) {
                        placeholderName = placeholder.substring(2, bracket);
                        expression = placeholder.substring(bracket + 1, placeholder.indexOf(')', bracket));
                    }

                    final String name = placeholderName;
                    Optional<String> optionalValue = placeholderValueSuppliers.getOrDefault(name, r -> Optional.of(name)).apply(expression);
                    String value = optionalValue.orElse(defaultValue);
                    StringBuilder sb = new StringBuilder(input);
                    String output = sb.replace(start, end+1, value).toString();
                    somethingReplaced = !Objects.equals(input, output);
                    input = output;
                }
            }
        } while (somethingReplaced);
        return input;
    }

    private Optional<String> replaceUtcDateTime(Recording rec, String pattern) {
        return replaceDateTime(rec, pattern, ZoneOffset.UTC);
    }

    private Optional<String> replaceLocalDateTime(Recording rec, String filename) {
        return replaceDateTime(rec, filename, ZoneId.systemDefault());
    }

    private Optional<String> replaceDateTime(Recording rec, String pattern, ZoneId zone) {
        pattern = pattern != null ? pattern : "yyyy-MM-dd_HH-mm-ss";
        return getDateTime(rec, pattern, zone);
    }

    private Optional<String> getDateTime(Recording rec, String pattern, ZoneId zone) {
        return Optional.ofNullable(rec.getStartDate()) //
                .map(DateTimeFormatter.ofPattern(pattern) //
                        .withLocale(Locale.getDefault()) //
                        .withZone(zone) //
                        ::format);
    }

    private Optional<String> getFileSuffix(Recording rec) {
        if (rec.isSingleFile()) {
            String filename = rec.getPostProcessedFile().getName();
            return Optional.of(filename.substring(filename.lastIndexOf('.') + 1));
        } else {
            return Optional.empty();
        }
    }

    private Optional<String> getSanitizedSiteName(Recording rec) {
        Optional<String> name = ofNullable(rec.getModel().getSite()).map(Site::getName);
        if (name.isPresent()) {
            return Optional.of(sanitize(name.get()));
        } else {
            return Optional.empty();
        }
    }

    private Optional<String> getSanitizedRecordingNotes(Recording rec) {
        Optional<String> notes = ofNullable(rec.getNote());
        if (notes.isPresent()) {
            return Optional.of(sanitize(notes.get()));
        } else {
            return Optional.empty();
        }
    }

    private Optional<String> getSanitizedModelNotes(Config config, Model m) {
        Optional<String> notes = ofNullable(config.getModelNotes(m));
        if (notes.isPresent()) {
            return Optional.of(sanitize(notes.get()));
        } else {
            return Optional.empty();
        }
    }
}
