using agxSensor;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace AGXUnity.Sensor
{
  [Serializable]
  public class LidarOutput : IList<RtOutput.Field>
  {
    public RtOutput Native { get; private set; } = null;

    public int Count => m_fields.Count;

    public bool IsReadOnly => false;

    public RtOutput.Field this[ int index ]
    {
      get => m_fields[ index ];
      set
      {
        if ( Native != null )
          throw new InvalidOperationException( "Modifying output fields at runtime is not supported!" );
        m_fields[ index ] = value;
      }
    }

    private LidarSensor m_parent = null;

    [SerializeField]
    private List<RtOutput.Field> m_fields = new List<RtOutput.Field>();

    private uint m_outputID = 0; // Must be greater than 0 to be valid

    /// <summary>
    /// Creates a new output and adds any provided fields to it.
    /// </summary>
    /// <param name="fields">Fields to add to the output.</param>
    public LidarOutput( params RtOutput.Field[] fields )
    {
      foreach ( var field in fields )
        Add( field );
    }

    internal bool Initialize( LidarSensor sensor )
    {
      if ( Native != null ) {
        if ( m_parent == sensor )
          return true;

        Debug.LogError( "A single LidarOutput instance was added to multiple LidarSensor components. This is not supported, use separate outputs instead." );
        return false;
      }

      m_parent = sensor;

      m_outputID = SensorEnvironment.Instance.GenerateOutputID();

      Native = new RtOutput();
      foreach ( var field in m_fields )
        Native.add( field );

      sensor.Native.getOutputHandler().add( m_outputID, Native );

      return true;
    }

    internal void Disconnect()
    {
      if ( Native != null && m_parent != null )
        m_parent.Native.getOutputHandler().removeChild( Native );
      Native?.Dispose();
      Native = null;
      m_parent = null;
      m_outputID = 0;
    }

    /// <summary>
    /// Returns the data generated by this output. The <paramref name="old"/> parameter might or might 
    /// not be overwritten and returned depending on the size of the provided array as well as the size of the output so always 
    /// use the returned value.
    /// </summary>
    /// <typeparam name="T">The element type of the array to return the data as. The size of the elements has to match the size 
    /// of the provided output fields.</typeparam>
    /// <param name="count">The number of elements in the output. Note that this might be smaller than the size of the output 
    /// array if an <paramref name="old"/> array of greater size is provided</param>
    /// <param name="old">An optional old array to write into if it contains enough elements to hold the output</param>
    /// <returns>An array of elements of the specified type of the output points generated by this output. 
    /// Not that the array might contain more elements than generated if a larger array was provided with <paramref name="old"/>.
    /// Always use the output <paramref name="count"/> to check the number of points returned.</returns>
    public T[] View<T>( out uint count, T[] old = null ) where T : struct
    {
      if ( Native == null ) {
        count = 0;
        return null;
      }

      return Native.View( out count, old );
    }

    /// <summary>
    /// Adds an <see cref="RtOutput.Field"/> to this output. Note that it is not supported to add fields after the output has been initialized
    /// (added to a sensor).
    /// </summary>
    /// <param name="field">The <see cref="RtOutput.Field"/> to add to this output.</param>
    /// <returns>True if the field was successfully added to the output.</returns>
    public bool Add( RtOutput.Field field )
    {
      if ( Native != null ) {
        Debug.LogError( "Adding output fields at runtime is not supported" );
        return false;
      }
      m_fields.Add( field );
      return true;
    }

    /// <summary>
    /// Removes an <see cref="RtOutput.Field"/> from this output. Note that it is not supported to remove fields after the output has been initialized
    /// (added to a sensor).
    /// </summary>
    /// <param name="field">The <see cref="RtOutput.Field"/> to remove from this output</param>
    /// <returns>True if the field was successfully removed from the output.</returns>
    public bool Remove( RtOutput.Field field )
    {
      if ( !m_fields.Contains( field ) )
        return false;
      if ( Native != null ) {
        Debug.LogError( "Removing output fields at runtime is not supported" );
        return false;
      }
      m_fields.Remove( field );
      return true;
    }

    public IEnumerator<RtOutput.Field> GetEnumerator() => m_fields.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => m_fields.GetEnumerator();

    public int IndexOf( RtOutput.Field item ) => m_fields.IndexOf( item );

    public void Insert( int index, RtOutput.Field item )
    {
      if ( Native != null )
        throw new InvalidOperationException( "Inserting output fields at runtime is not supported." );
      m_fields.Insert( index, item );
    }

    public void RemoveAt( int index )
    {
      if ( Native != null )
        throw new InvalidOperationException( "Removing output fields at runtime is not supported." );
      m_fields.RemoveAt( index );
    }

    void ICollection<RtOutput.Field>.Add( RtOutput.Field item )
    {
      if ( Native != null )
        throw new InvalidOperationException( "Adding output fields at runtime is not supported." );
      m_fields.Add( item );
    }

    public void Clear()
    {
      if ( Native != null )
        throw new InvalidOperationException( "Clearing output fields at runtime is not supported." );
      m_fields.Clear();
    }

    public bool Contains( RtOutput.Field item ) => m_fields.Contains( item );

    public void CopyTo( RtOutput.Field[] array, int arrayIndex ) => m_fields.CopyTo( array, arrayIndex );
  }
}
