import 'dart:convert';

import 'package:guru_utils/extensions/extensions.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_utils/property/app_property.dart';
import 'package:guru_utils/property/property.dart';

/// Created by @Haoyi on 4/19/21
///

class DeclareSettingDataException implements Exception {
  final String msg;

  DeclareSettingDataException(this.msg);

  @override
  String toString() {
    return "[DeclareSettingDataException] $msg";
  }
}

abstract class SettingData<T> {
  static final Map<PropertyKey, SettingData> settings = {};

  const SettingData();

  Stream<T> observe();

  void init(PropertyBundle bundle);

  void set(T? data);

  T get({T? overrideDefaultValue});

  SettingData<int> toIntData(int Function(T) _convert, T Function(int) _revert) {
    return ConvertibleSettingData(this, _convert, _revert);
  }

  SettingData<bool> toBoolData(bool Function(T) _convert, T Function(bool) _revert) {
    return ConvertibleSettingData(this, _convert, _revert);
  }

  SettingData<String> toStringData(String Function(T) _convert, T Function(String) _revert) {
    return ConvertibleSettingData(this, _convert, _revert);
  }

  SettingData<double> toDoubleData(double Function(T) _convert, T Function(double) _revert) {
    return ConvertibleSettingData(this, _convert, _revert);
  }
}

class ConstantSettingData<T> extends SettingData<T> {
  final T value;

  const ConstantSettingData(this.value);

  @override
  void init(PropertyBundle bundle) {}

  @override
  Stream<T> observe() => Stream.value(value);

  @override
  void set(T? data) {}

  @override
  T get({T? overrideDefaultValue}) {
    return value;
  }
}

abstract class UniformSettingData<T> extends SettingData<T> {
  final T defaultValue;
  final PropertyKey name;
  final BehaviorSubject<T> subject;

  UniformSettingData(this.name, {required this.defaultValue})
      : subject = BehaviorSubject.seeded(defaultValue) {
    SettingData.settings[name] = this;
  }

  @override
  Stream<T> observe() => subject.stream;

  @override
  void init(PropertyBundle bundle);

  @override
  void set(T? data) {
    subject.addEx(data ?? defaultValue);
  }

  @override
  T get({T? overrideDefaultValue}) {
    return subject.value ?? overrideDefaultValue ?? defaultValue;
  }
}

class ConvertibleSettingData<Src, Dst> extends SettingData<Dst> {
  final SettingData<Src> _origin;
  final Dst Function(Src) _convert;
  final Src Function(Dst) _revert;

  ConvertibleSettingData(this._origin, this._convert, this._revert);

  @override
  Stream<Dst> observe() => _origin.observe().map((event) => _convert(event));

  @override
  void set(Dst? data) {
    _origin.set(data == null ? null : _revert(data));
  }

  @override
  Dst get({Dst? overrideDefaultValue}) {
    return _convert(_origin.get(
        overrideDefaultValue: overrideDefaultValue == null ? null : _revert(overrideDefaultValue)));
  }

  @override
  void init(PropertyBundle bundle) {}
}

class SettingIntData extends UniformSettingData<int> {
  static SettingIntData _getInstance(PropertyKey key, {required int defaultValue}) {
    final data = SettingData.settings[key];
    if (data != null) {
      if (data is SettingIntData) {
        return data;
      } else {
        throw DeclareSettingDataException(
            "declare $key to SettingIntData error! already exists same SettingData(${data.runtimeType})!");
      }
    }
    return SettingIntData._(key, defaultValue: defaultValue);
  }

  SettingIntData._(PropertyKey name, {required int defaultValue})
      : super(name, defaultValue: defaultValue);

  factory SettingIntData(PropertyKey name, {required int defaultValue}) =>
      _getInstance(name, defaultValue: defaultValue);

  @override
  void set(int? data) {
    super.set(data);
    if (data == null) {
      AppProperty.getInstance().remove(name);
    } else {
      AppProperty.getInstance().setInt(name, data);
    }
  }

  @override
  void init(PropertyBundle bundle) {
    super.set(bundle.getInt(name));
  }
}

class SettingDoubleData extends UniformSettingData<double> {
  static SettingDoubleData _getInstance(PropertyKey key, {required double defaultValue}) {
    final data = SettingData.settings[key];
    if (data != null) {
      if (data is SettingDoubleData) {
        return data;
      } else {
        throw DeclareSettingDataException(
            "declare $key to SettingDoubleData error! already exists same SettingData(${data.runtimeType})!");
      }
    }
    return SettingDoubleData._(key, defaultValue: defaultValue);
  }

  SettingDoubleData._(PropertyKey name, {required double defaultValue})
      : super(name, defaultValue: defaultValue);

  factory SettingDoubleData(PropertyKey name, {required double defaultValue}) =>
      _getInstance(name, defaultValue: defaultValue);

  @override
  void init(PropertyBundle bundle) {
    super.set(bundle.getDouble(name));
  }

  @override
  void set(double? data) {
    super.set(data);
    if (data == null) {
      AppProperty.getInstance().remove(name);
    } else {
      AppProperty.getInstance().setDouble(name, data);
    }
  }
}

class SettingStringData extends UniformSettingData<String> {
  static SettingStringData _getInstance(PropertyKey key, {required String defaultValue}) {
    final data = SettingData.settings[key];
    if (data != null) {
      if (data is SettingStringData) {
        return data;
      } else {
        throw DeclareSettingDataException(
            "declare $key to SettingStringData error! already exists same SettingData(${data.runtimeType})!");
      }
    }
    return SettingStringData._(key, defaultValue: defaultValue);
  }

  SettingStringData._(PropertyKey name, {required String defaultValue})
      : super(name, defaultValue: defaultValue);

  factory SettingStringData(PropertyKey name, {required String defaultValue}) =>
      _getInstance(name, defaultValue: defaultValue);

  @override
  void init(PropertyBundle bundle) {
    super.set(bundle.getString(name));
  }

  @override
  void set(String? data) {
    super.set(data);
    if (data == null) {
      AppProperty.getInstance().remove(name);
    } else {
      AppProperty.getInstance().setString(name, data);
    }
  }
}

class SettingBoolData extends UniformSettingData<bool> {
  static SettingBoolData _getInstance(PropertyKey key, {required bool defaultValue}) {
    final data = SettingData.settings[key];
    if (data != null) {
      if (data is SettingBoolData) {
        return data;
      } else {
        throw DeclareSettingDataException(
            "declare $key to SettingBoolData error! already exists same SettingData(${data.runtimeType})!");
      }
    }
    return SettingBoolData._(key, defaultValue: defaultValue);
  }

  SettingBoolData._(PropertyKey name, {required bool defaultValue})
      : super(name, defaultValue: defaultValue);

  factory SettingBoolData(PropertyKey name, {required bool defaultValue}) =>
      _getInstance(name, defaultValue: defaultValue);

  @override
  void init(PropertyBundle bundle) {
    super.set(bundle.getBool(name));
  }

  @override
  void set(bool? data) {
    super.set(data);
    if (data == null) {
      AppProperty.getInstance().remove(name);
    } else {
      AppProperty.getInstance().setBool(name, data);
    }
  }
}

class SettingJsonData<T> extends UniformSettingData<T> {
  final T Function(Map<String, dynamic>) decoder;

  static SettingJsonData<T> _getInstance<T>(PropertyKey key,
      {required T Function(Map<String, dynamic>) decoder, required T defaultValue}) {
    final data = SettingData.settings[key];
    if (data != null) {
      if (data is SettingJsonData<T>) {
        return data;
      } else {
        throw DeclareSettingDataException(
            "declare $key to SettingJsonData error! already exists same SettingData(${data.runtimeType})!");
      }
    }
    return SettingJsonData<T>._(key, decoder: decoder, defaultValue: defaultValue);
  }

  factory SettingJsonData(PropertyKey name,
          {required T Function(Map<String, dynamic>) decoder, required T defaultValue}) =>
      _getInstance(name, decoder: decoder, defaultValue: defaultValue);

  SettingJsonData._(PropertyKey name, {required this.decoder, required T defaultValue})
      : super(name, defaultValue: defaultValue);

  @override
  void init(PropertyBundle bundle) {
    final jsonStr = bundle.getString(name);
    if (jsonStr != null) {
      try {
        final jsonMap = json.decode(jsonStr);
        super.set(decoder.call(jsonMap));
        return;
      } catch (error, stacktrace) {
        Log.d("init settingJsonData error:$error, $stacktrace");
        super.set(defaultValue);
      }
    }
    super.set(defaultValue);
  }

  @override
  void set(T? data) {
    super.set(data);
    if (data == null) {
      AppProperty.getInstance().remove(name);
    } else {
      final jsonData = json.encode(data);
      AppProperty.getInstance().setString(name, jsonData);
    }
  }
}

class RuntimeSettingIntData extends UniformSettingData<int> {
  static RuntimeSettingIntData _getInstance(PropertyKey key, {required int defaultValue}) {
    final data = SettingData.settings[key];
    if (data != null) {
      if (data is RuntimeSettingIntData) {
        return data;
      } else {
        throw DeclareSettingDataException(
            "declare $key to SettingIntData error! already exists same SettingData(${data.runtimeType})!");
      }
    }
    return RuntimeSettingIntData._(key, defaultValue: defaultValue);
  }

  RuntimeSettingIntData._(PropertyKey name, {required int defaultValue})
      : super(name, defaultValue: defaultValue);

  factory RuntimeSettingIntData(PropertyKey name, {required int defaultValue}) =>
      _getInstance(name, defaultValue: defaultValue);

  @override
  void set(int? data) {
    super.set(data);
    if (data == null) {
      RuntimeProperty.instance.remove(name);
    } else {
      RuntimeProperty.instance.setInt(name, data);
    }
  }

  @override
  void init(PropertyBundle bundle) {
    super.set(bundle.getInt(name));
  }
}

class RuntimeSettingDoubleData extends UniformSettingData<double> {
  static RuntimeSettingDoubleData _getInstance(PropertyKey key, {required double defaultValue}) {
    final data = SettingData.settings[key];
    if (data != null) {
      if (data is RuntimeSettingDoubleData) {
        return data;
      } else {
        throw DeclareSettingDataException(
            "declare $key to RuntimeSettingDoubleData error! already exists same SettingData(${data.runtimeType})!");
      }
    }
    return RuntimeSettingDoubleData._(key, defaultValue: defaultValue);
  }

  RuntimeSettingDoubleData._(PropertyKey name, {required double defaultValue})
      : super(name, defaultValue: defaultValue);

  factory RuntimeSettingDoubleData(PropertyKey name, {required double defaultValue}) =>
      _getInstance(name, defaultValue: defaultValue);

  @override
  void init(PropertyBundle bundle) {
    super.set(bundle.getDouble(name));
  }

  @override
  void set(double? data) {
    super.set(data);
    if (data == null) {
      RuntimeProperty.instance.remove(name);
    } else {
      RuntimeProperty.instance.setDouble(name, data);
    }
  }
}

class RuntimeSettingStringData extends UniformSettingData<String> {
  static RuntimeSettingStringData _getInstance(PropertyKey key, {required String defaultValue}) {
    final data = SettingData.settings[key];
    if (data != null) {
      if (data is RuntimeSettingStringData) {
        return data;
      } else {
        throw DeclareSettingDataException(
            "declare $key to RuntimeSettingStringData error! already exists same SettingData(${data.runtimeType})!");
      }
    }
    return RuntimeSettingStringData._(key, defaultValue: defaultValue);
  }

  RuntimeSettingStringData._(PropertyKey name, {required String defaultValue})
      : super(name, defaultValue: defaultValue);

  factory RuntimeSettingStringData(PropertyKey name, {required String defaultValue}) =>
      _getInstance(name, defaultValue: defaultValue);

  @override
  void init(PropertyBundle bundle) {
    super.set(bundle.getString(name));
  }

  @override
  void set(String? data) {
    super.set(data);
    if (data == null) {
      RuntimeProperty.instance.remove(name);
    } else {
      RuntimeProperty.instance.setString(name, data);
    }
  }
}

class RuntimeSettingBoolData extends UniformSettingData<bool> {
  static RuntimeSettingBoolData _getInstance(PropertyKey key, {required bool defaultValue}) {
    final data = SettingData.settings[key];
    if (data != null) {
      if (data is RuntimeSettingBoolData) {
        return data;
      } else {
        throw DeclareSettingDataException(
            "declare $key to RuntimeSettingBoolData error! already exists same SettingData(${data.runtimeType})!");
      }
    }
    return RuntimeSettingBoolData._(key, defaultValue: defaultValue);
  }

  RuntimeSettingBoolData._(PropertyKey name, {required bool defaultValue})
      : super(name, defaultValue: defaultValue);

  factory RuntimeSettingBoolData(PropertyKey name, {required bool defaultValue}) =>
      _getInstance(name, defaultValue: defaultValue);

  @override
  void init(PropertyBundle bundle) {
    super.set(bundle.getBool(name));
  }

  @override
  void set(bool? data) {
    super.set(data);
    if (data == null) {
      RuntimeProperty.instance.remove(name);
    } else {
      RuntimeProperty.instance.setBool(name, data);
    }
  }
}

class RuntimeSettingJsonData<T> extends UniformSettingData<T> {
  final T Function(Map<String, dynamic>) decoder;

  static RuntimeSettingJsonData<T> _getInstance<T>(PropertyKey key,
      {required T Function(Map<String, dynamic>) decoder, required T defaultValue}) {
    final data = SettingData.settings[key];
    if (data != null) {
      if (data is RuntimeSettingJsonData<T>) {
        return data;
      } else {
        throw DeclareSettingDataException(
            "declare $key to SettingJsonData error! already exists same SettingData(${data.runtimeType})!");
      }
    }
    return RuntimeSettingJsonData<T>._(key, decoder: decoder, defaultValue: defaultValue);
  }

  factory RuntimeSettingJsonData(PropertyKey name,
          {required T Function(Map<String, dynamic>) decoder, required T defaultValue}) =>
      _getInstance(name, decoder: decoder, defaultValue: defaultValue);

  RuntimeSettingJsonData._(PropertyKey name, {required this.decoder, required T defaultValue})
      : super(name, defaultValue: defaultValue);

  @override
  void init(PropertyBundle bundle) {
    final jsonStr = bundle.getString(name);
    if (jsonStr != null) {
      try {
        final jsonMap = json.decode(jsonStr);
        super.set(decoder.call(jsonMap));
        return;
      } catch (error, stacktrace) {
        Log.d("init settingJsonData error:$error, $stacktrace");
        super.set(defaultValue);
      }
    }
    super.set(defaultValue);
  }

  @override
  void set(T? data) {
    super.set(data);
    if (data == null) {
      RuntimeProperty.instance.remove(name);
    } else {
      final jsonData = json.encode(data);
      RuntimeProperty.instance.setString(name, jsonData);
    }
  }
}
