﻿using System;
using System.Threading;
using System.Threading.Tasks;

namespace WeakRefUtils
{
    public class WeakLazy<T> where T : class
    {
        private readonly object CreateLock;
        private readonly Func<T> Creator;
        private readonly LazyThreadSafetyMode Mode;
        private Exception ThrowCache;
        private readonly WeakReference _Value;

        public WeakLazy() : this(LazyThreadSafetyMode.ExecutionAndPublication) { }
        public WeakLazy(bool isThreadSafe) : this(isThreadSafe ? LazyThreadSafetyMode.ExecutionAndPublication : LazyThreadSafetyMode.None) { }
        public WeakLazy(Func<T> valueFactory) : this(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication) { }
        public WeakLazy(LazyThreadSafetyMode mode)
        {
            if (mode != LazyThreadSafetyMode.None)
                CreateLock = new object();
            Creator = Activator.CreateInstance<T>;
            Mode = mode;
            _Value = new WeakReference(null);
        }
        public WeakLazy(bool isThreadSafe, bool trackResurrection) : this(isThreadSafe ? LazyThreadSafetyMode.ExecutionAndPublication : LazyThreadSafetyMode.None) { }
        public WeakLazy(Func<T> valueFactory, bool trackResurrection) : this(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication, trackResurrection) { }
        public WeakLazy(Func<T> valueFactory, LazyThreadSafetyMode mode)
        {
            if (valueFactory == null)
                throw new ArgumentNullException(nameof(valueFactory));

            if (mode != LazyThreadSafetyMode.None)
                CreateLock = new object();
            Creator = valueFactory;
            Mode = mode;
            _Value = new WeakReference(null);
        }
        public WeakLazy(LazyThreadSafetyMode mode, bool trackResurrection)
        {
            if (mode != LazyThreadSafetyMode.None)
                CreateLock = new object();
            Creator = Activator.CreateInstance<T>;
            Mode = mode;
            _Value = new WeakReference(null, trackResurrection);
        }
        public WeakLazy(Func<T> valueFactory, LazyThreadSafetyMode mode, bool trackResurrection)
        {
            if (valueFactory == null)
                throw new ArgumentNullException(nameof(valueFactory));

            if (mode != LazyThreadSafetyMode.None)
                CreateLock = new object();
            Creator = valueFactory;
            Mode = mode;
            _Value = new WeakReference(null, trackResurrection);
        }

        public bool IsValueCreated => _Value.IsAlive;
        public T Value
        {
            get
            {
                var value = TryGetValue();
                if (value != null)
                    return value;

                return CreateValue();
            }
        }

        public async Task<T> GetOrCreateValueAsync()
        {
            var value = TryGetValue();
            if (value != null)
                return value;

            return await Task.Run((Func<T>)CreateValue);
        }

        public override string ToString() => _Value.Target?.ToString();

        private T CreateValue()
        {
            T value;
            switch (Mode)
            {
                case LazyThreadSafetyMode.ExecutionAndPublication:
                    lock (CreateLock)
                    {
                        var v2 = _TryGetValue();
                        if (v2 != null)
                            return v2;
                        try
                        {
                            value = Creator();
                        }
                        catch (Exception e)
                        {
                            if (Creator != Activator.CreateInstance<T>)
                                ThrowCache = e; // 発生した例外をキャッシュ
                            throw;
                        }
                        _Value.Target = value;
                    }
                    return value;

                case LazyThreadSafetyMode.None:
                    value = Creator();
                    _Value.Target = value;
                    return value;

                case LazyThreadSafetyMode.PublicationOnly:
                    value = Creator();
                    lock (CreateLock)
                    {
                        var v2 = (T)_Value.Target;
                        if (v2 == null)
                        {
                            _Value.Target = value;
                            return value;
                        }
                        return v2;
                    }

                default:
                    throw new NotSupportedException();
            }
        }

        private T TryGetValue()
        {
            if (Mode == LazyThreadSafetyMode.ExecutionAndPublication && Monitor.IsEntered(CreateLock))
                throw new InvalidOperationException(); // 再帰的に呼び出された場合は例外
            return _TryGetValue();
        }

        private T _TryGetValue()
        {
            if (ThrowCache != null)
                throw ThrowCache; // キャッシュされた例外を投げる
            return (T)_Value.Target;
        }
    }
}
