package app

import (
	
	
	
	

	
	
)

// State represents a state with additional features such as expiration,
// persistence, and broadcasting capabilities.
type State struct {
	value     any
	expiresAt time.Time

	ctx       Context
	name      string
	expire    func(State, time.Time) State
	persist   func(State, bool) State
	broadcast func(State) State
}

// ExpiresIn sets the expiration time for the state by specifying a duration
// from the current time.
func ( State) ( time.Duration) State {
	return .expire(, time.Now().Add())
}

// ExpiresAt sets the exact expiration time for the state.
func ( State) ( time.Time) State {
	return .expire(, )
}

// Persist ensures the state is persisted into the local storage.
func ( State) () State {
	return .persist(, false)
}

// PersistWithEncryption ensures the state is persisted into the local storage
// with encryption.
func ( State) () State {
	return .persist(, true)
}

// Broadcast signals that changes to the state will be broadcasted to other
// browser tabs and windows sharing the same origin when it is supported.
//
// Using Broadcast creates a BroadcastChannel, which prevents the page from
// being cached. This may impact the Chrome Lighthouse performance score due to
// the additional resources required to manage the broadcast channel.
func ( State) () State {
	return .broadcast()
}

type storableState struct {
	Value          json.RawMessage `json:",omitempty"`
	EncryptedValue []byte          `json:",omitempty"`
	ExpiresAt      time.Time       `json:",omitempty"`
}

// Observer represents a mechanism to monitor and react to changes in a state.
type Observer struct {
	source        UI
	receiver      any
	condition     func() bool
	changeHandler func()
	broadcast     bool

	state           string
	setObserver     func(Observer) Observer
	enableBroadcast func()
}

// While sets a condition for the observer, determining whether it observes
// a state. The condition is periodically checked. Observation stops when the
// condition returns false.
func ( Observer) ( func() bool) Observer {
	.condition = 
	return .setObserver()
}

// OnChange sets a callback function to be executed each time the observer
// detects a change in the associated state value.
func ( Observer) ( func()) Observer {
	.changeHandler = 
	return .setObserver()
}

// WithBroadcast enables the observer to listen to state changes that are
// broadcasted by other browser tabs or windows. This is useful for s
// ynchronizing state across multiple open instances of a web application within
// the same browser.
//
// Calling WithBroadcast creates a BroadcastChannel, which prevents the page
// from being cached. This may impact the Chrome Lighthouse performance
// score due to the additional resources required to manage the broadcast
// channel.
func ( Observer) () Observer {
	.enableBroadcast()
	.broadcast = true
	return .setObserver()
}

func ( Observer) () bool {
	if .source == nil || !.source.Mounted() {
		return false
	}
	if .condition != nil {
		return .condition()
	}
	return true
}

// stateManager is responsible for managing, tracking, and notifying changes
// to state values. It supports concurrency-safe operations and provides
// functionality to observe state changes.
type stateManager struct {
	mutex             sync.RWMutex
	states            map[string]State
	observers         map[string]map[UI]Observer
	initBroadcastOnce sync.Once
	broadcastStoreID  string
	broadcastChannel  Value
}

// Observe initiates observation for a specified state, ensuring the state
// is fetched and set into the given receiver. The returned observer object
// offers methods for advanced observation configurations.
func ( *stateManager) ( Context,  string,  any) Observer {
	.Get(, , )

	return .setObserver(Observer{
		source:          .Src(),
		receiver:        ,
		state:           ,
		setObserver:     .setObserver,
		enableBroadcast: func() { .initBroadcast() },
	})
}

func ( *stateManager) ( Observer) Observer {
	.mutex.Lock()
	defer .mutex.Unlock()

	if .observers == nil {
		.observers = make(map[string]map[UI]Observer)
	}

	 := .observers[.state]
	if  == nil {
		 = map[UI]Observer{}
		.observers[.state] = 
	}
	[.source] = Observer{
		source:        .source,
		receiver:      .receiver,
		condition:     .condition,
		changeHandler: .changeHandler,
		broadcast:     .broadcast,
	}

	return 
}

// Get retrieves the value of a specific state, setting it to the provided
// receiver.
func ( *stateManager) ( Context,  string,  any) {
	.mutex.Lock()
	defer .mutex.Unlock()

	,  := .states[]
	if ! {
		if  := .getStoredState(, , );  != nil {
			Log(errors.New("getting state from local storage failed").
				WithTag("state", ).
				Wrap())
		}
		return
	}

	if expiredTime(.expiresAt) {
		delete(.states, )
		.LocalStorage().Del()
		return
	}

	if  := storeValue(, .value);  != nil {
		Log(errors.New("getting state failed").
			WithTag("state", ).
			Wrap())
	}
}

func ( *stateManager) ( Context,  string,  any) error {
	var  storableState
	if  := .LocalStorage().Get(, &);  != nil {
		return 
	}

	if expiredTime(.ExpiresAt) {
		.LocalStorage().Del()
		return nil
	}

	if len(.EncryptedValue) != 0 {
		return .Decrypt(.EncryptedValue, )
	} else if len(.Value) != 0 {
		return json.Unmarshal(.Value, )
	}
	return nil
}

func ( *stateManager) ( Context,  string,  any) State {
	.mutex.Lock()
	defer .mutex.Unlock()

	if .states == nil {
		.states = make(map[string]State)
	}

	 := State{value: }
	.states[] = 

	for ,  := range .observers[] {
		 := 
		.sourceElement = .source

		.Dispatch(func( Context) {
			.mutex.RLock()
			 := .states[]
			.mutex.RUnlock()

			if expiredTime(.expiresAt) {
				return
			}

			if !.observing() {
				.mutex.Lock()
				delete(.observers[], .source)
				.mutex.Unlock()
				return
			}

			if  := storeValue(.receiver, .value);  != nil {
				Log(errors.New("storing state value into receiver failed").
					WithTag("state", ).
					WithTag("observer-type", reflect.TypeOf(.source)).
					WithTag("receiver-type", reflect.TypeOf(.receiver)).
					Wrap())
				return
			}

			if .changeHandler != nil {
				.changeHandler()
			}
		})
	}

	return State{
		value:     ,
		ctx:       ,
		name:      ,
		expire:    .setExpiration,
		persist:   .persist,
		broadcast: .broadcast,
	}
}

// Set updates a specified state with a new value and notifies its observers.
// It returns a state object, offering methods for advanced state manipulations.
func ( *stateManager) ( State,  time.Time) State {
	.mutex.Lock()
	defer .mutex.Unlock()

	.expiresAt = 

	 := .states[.name]
	.expiresAt = 
	.states[.name] = 

	return 
}

func ( *stateManager) ( State,  bool) State {
	.mutex.Lock()
	defer .mutex.Unlock()

	 := storableState{ExpiresAt: .expiresAt}
	if  {
		,  := .ctx.Encrypt(.value)
		if  != nil {
			Log(errors.New("persisting encrypted state failed").
				WithTag("state", .name).
				Wrap())
			return 
		}
		.EncryptedValue = 
	} else {
		,  := json.Marshal(.value)
		if  != nil {
			Log(errors.New("persisting state failed").
				WithTag("state", .name).
				Wrap())
			return 
		}
		.Value = 
	}

	if  := .ctx.LocalStorage().Set(.name, );  != nil {
		Log(errors.New("persisting state failed").
			WithTag("state", .name).
			Wrap())
	}
	return 
}

func ( *stateManager) ( State) State {
	.mutex.Lock()
	defer .mutex.Unlock()

	.initBroadcast(.ctx)

	if .broadcastChannel == nil {
		Log(errors.New("broadcast not supported").
			WithTag("state", .name))
		return 
	}

	,  := json.Marshal(.value)
	if  != nil {
		Log(errors.New("encoding broadcast state failed").
			WithTag("state", .name).
			Wrap())
		return 
	}

	.broadcastChannel.Call("postMessage", map[string]any{
		"StoreID": .broadcastStoreID,
		"State":   .name,
		"Value":   string(),
	})
	return 
}

func ( *stateManager) ( Context) {
	.initBroadcastOnce.Do(func() {
		 := Window().Get("BroadcastChannel")
		if !.Truthy() {
			return
		}
		 = .New("go-app-broadcast-states")
		.broadcastChannel = 
		.broadcastStoreID = uuid.NewString()

		 := FuncOf(func( Value,  []Value) any {
			.handleBroadcast(, [0].Get("data"))
			return nil
		})
		.Set("onmessage", )
	})
}

func ( *stateManager) ( Context,  Value) {
	if  := .Get("StoreID").String();  == "" ||  == .broadcastStoreID {
		return
	}

	.mutex.Lock()
	defer .mutex.Unlock()

	 := .Get("State").String()
	 := []byte(.Get("Value").String())

	for ,  := range .observers[] {
		 := 
		if !.broadcast {
			continue
		}

		.sourceElement = .source
		.Dispatch(func( Context) {
			if !.observing() {
				.mutex.Lock()
				delete(.observers[], .source)
				.mutex.Unlock()
				return
			}

			if  := json.Unmarshal(, .receiver);  != nil {
				Log(errors.New("storing broadcast state value into receiver failed").
					WithTag("state", ).
					WithTag("observer-type", reflect.TypeOf(.source)).
					WithTag("receiver-type", reflect.TypeOf(.receiver)).
					Wrap())
			}

			if .changeHandler != nil {
				.changeHandler()
			}
		})
	}
}

// Delete removes the specified state from the managed states and also deletes
// it from the local storage if it was previously persisted.
func ( *stateManager) ( Context,  string) {
	.mutex.Lock()
	defer .mutex.Unlock()

	delete(.states, )
	.LocalStorage().Del()
}

// Cleanup removes observers that are no longer active and cleans up any states
// without observers.
func ( *stateManager) () {
	.mutex.Lock()
	defer .mutex.Unlock()

	for ,  := range .observers {
		for ,  := range  {
			if !.observing() {
				delete(, .source)
			}
		}

		if len() == 0 {
			delete(.observers, )
		}
	}
}

// CleanupExpiredPersistedStates traverses the local storage to identify and
// remove any persisted states that have expired. This method ensures that the
// local storage is kept clean by eliminating outdated or irrelevant state data.
func ( *stateManager) ( Context) {
	.LocalStorage().ForEach(func( string) {
		var  storableState
		.LocalStorage().Get(, &)
		if (len(.Value) != 0 || len(.EncryptedValue) != 0) &&
			expiredTime(.ExpiresAt) {
			.LocalStorage().Del()
		}
	})
}

func storeValue(,  any) error {
	 := reflect.ValueOf()
	if .Kind() != reflect.Ptr {
		return errors.New("receiver is not a pointer")
	}
	 = .Elem()

	 := reflect.ValueOf()
	switch {
	case  == reflect.Value{}:
		.Set(reflect.Zero(.Type()))
		return nil

	case .Kind() == reflect.Ptr:
		 = .Elem()
	}

	if .Type() != .Type() {
		return errors.New("value and receiver are not of the same type").
			WithTag("value-type", .Type()).
			WithTag("receiver-type", .Type())
	}
	.Set()
	return nil
}

func expiredTime( time.Time) bool {
	return !.IsZero() && .Before(time.Now())
}