/*
 * Copyright (C) 2014-2023 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.helger.css.decl;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.equals.EqualsHelper;
import com.helger.commons.hashcode.HashCodeGenerator;
import com.helger.commons.state.EChange;
import com.helger.commons.string.ToStringGenerator;
import com.helger.css.CSSSourceLocation;
import com.helger.css.ICSSSourceLocationAware;
import com.helger.css.ICSSWriteable;
import com.helger.css.ICSSWriterSettings;

/**
 * Represents a single media query
 *
 * @author Philip Helger
 */
@NotThreadSafe
public class CSSMediaQuery implements ICSSWriteable, ICSSSourceLocationAware
{
  /**
   * A global modifier that can be used in front of a single CSS media query.
   *
   * @author Philip Helger
   */
  public enum EModifier
  {
    NONE (""),
    // Trailing whitespace by purpose
    NOT ("not "),
    // Trailing whitespace by purpose
    ONLY ("only ");

    private final String m_sText;

    EModifier (@Nonnull final String sText)
    {
      m_sText = sText;
    }

    @Nonnull
    public String getCSSText ()
    {
      return m_sText;
    }
  }

  private final EModifier m_eModifier;
  private final String m_sMedium;
  private final ICommonsList <CSSMediaExpression> m_aMediaExpressions = new CommonsArrayList <> ();
  private CSSSourceLocation m_aSourceLocation;

  /**
   * Constructor without a modifier. This implicitly uses the modifier
   * {@link EModifier#NONE}.
   *
   * @param sMedium
   *        The medium to use. May be <code>null</code>.
   */
  public CSSMediaQuery (@Nullable final String sMedium)
  {
    this (EModifier.NONE, sMedium);
  }

  /**
   * Constructor
   *
   * @param eModifier
   *        The modifier to use. May not be <code>null</code>.
   * @param sMedium
   *        The medium to use. May be <code>null</code>.
   */
  public CSSMediaQuery (@Nonnull final EModifier eModifier, @Nullable final String sMedium)
  {
    ValueEnforcer.notNull (eModifier, "Modifier");
    m_eModifier = eModifier;
    m_sMedium = sMedium;
  }

  /**
   * @return The media query modifier that was used. Never <code>null</code>.
   */
  @Nonnull
  public final EModifier getModifier ()
  {
    return m_eModifier;
  }

  /**
   * @return <code>true</code> if the modifier is {@link EModifier#NOT}.
   * @see #getModifier()
   */
  public final boolean isNot ()
  {
    return m_eModifier == EModifier.NOT;
  }

  /**
   * @return <code>true</code> if the modifier is {@link EModifier#ONLY}.
   * @see #getModifier()
   */
  public final boolean isOnly ()
  {
    return m_eModifier == EModifier.ONLY;
  }

  /**
   * @return The medium passed in the constructor.
   */
  @Nullable
  public final String getMedium ()
  {
    return m_sMedium;
  }

  /**
   * @return <code>true</code> if at least a single media expression is present.
   */
  public boolean hasMediaExpressions ()
  {
    return m_aMediaExpressions.isNotEmpty ();
  }

  /**
   * @return The number of contained media expressions. Always &ge; 0.
   */
  @Nonnegative
  public int getMediaExpressionCount ()
  {
    return m_aMediaExpressions.size ();
  }

  /**
   * Append a media expression to the list.
   *
   * @param aMediaExpression
   *        The media expression to be added. May not be <code>null</code>.
   * @return this
   */
  @Nonnull
  public CSSMediaQuery addMediaExpression (@Nonnull final CSSMediaExpression aMediaExpression)
  {
    ValueEnforcer.notNull (aMediaExpression, "MediaExpression");

    m_aMediaExpressions.add (aMediaExpression);
    return this;
  }

  /**
   * Add a media expression to the list at the specified index.
   *
   * @param nIndex
   *        The index where the media expression should be added. Must be &ge;
   *        0.
   * @param aMediaExpression
   *        The media expression to be added. May not be <code>null</code>.
   * @return this
   */
  @Nonnull
  public CSSMediaQuery addMediaExpression (@Nonnegative final int nIndex, @Nonnull final CSSMediaExpression aMediaExpression)
  {
    ValueEnforcer.isGE0 (nIndex, "Index");
    ValueEnforcer.notNull (aMediaExpression, "MediaExpression");

    if (nIndex >= getMediaExpressionCount ())
      m_aMediaExpressions.add (aMediaExpression);
    else
      m_aMediaExpressions.add (nIndex, aMediaExpression);
    return this;
  }

  /**
   * Remove the specified media expression.
   *
   * @param aMediaExpression
   *        The media expression to be removed. May be <code>null</code>.
   * @return {@link EChange#CHANGED} if removal succeeded,
   *         {@link EChange#UNCHANGED} otherwise.
   */
  @Nonnull
  public EChange removeMediaExpression (@Nullable final CSSMediaExpression aMediaExpression)
  {
    return m_aMediaExpressions.removeObject (aMediaExpression);
  }

  /**
   * Remove the media expression at the specified index.
   *
   * @param nExpressionIndex
   *        The index of the media expression to be removed.
   * @return {@link EChange#CHANGED} if removal succeeded,
   *         {@link EChange#UNCHANGED} otherwise.
   */
  @Nonnull
  public EChange removeMediaExpression (final int nExpressionIndex)
  {
    return m_aMediaExpressions.removeAtIndex (nExpressionIndex);
  }

  /**
   * Remove all media expressions.
   *
   * @return {@link EChange#CHANGED} if any media expression was removed,
   *         {@link EChange#UNCHANGED} otherwise. Never <code>null</code>.
   * @since 3.7.3
   */
  @Nonnull
  public EChange removeAllMediaExpressions ()
  {
    return m_aMediaExpressions.removeAll ();
  }

  /**
   * Get the media expression at the specified index.
   *
   * @param nExpressionIndex
   *        The index to be retrieved.
   * @return <code>null</code> if the index is &lt; 0 or too large.
   */
  @Nullable
  public CSSMediaExpression getMediaExpression (@Nonnegative final int nExpressionIndex)
  {
    return m_aMediaExpressions.getAtIndex (nExpressionIndex);
  }

  /**
   * @return A copy of all media expression. Never <code>null</code> but maybe
   *         empty.
   */
  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList <CSSMediaExpression> getAllMediaExpressions ()
  {
    return m_aMediaExpressions.getClone ();
  }

  @Nonnull
  @Nonempty
  public String getAsCSSString (@Nonnull final ICSSWriterSettings aSettings, @Nonnegative final int nIndentLevel)
  {
    final StringBuilder aSB = new StringBuilder ();

    // The modifier already contains a trailing space if necessary!
    aSB.append (m_eModifier.getCSSText ());

    boolean bIsFirstExpression = true;
    if (m_sMedium != null)
    {
      // Medium is optional
      aSB.append (m_sMedium);
      bIsFirstExpression = false;
    }

    if (m_aMediaExpressions.isNotEmpty ())
    {
      for (final CSSMediaExpression aMediaExpression : m_aMediaExpressions)
      {
        if (bIsFirstExpression)
          bIsFirstExpression = false;
        else
        {
          // The leading blank is required in case this is the first expression
          // after a medium declaration ("projectorand" instead of
          // "projector and")!
          // The trailing blank is required, because otherwise it is considered
          // a function ("and(")!
          aSB.append (" and ");
        }
        aSB.append (aMediaExpression.getAsCSSString (aSettings, nIndentLevel));
      }
    }

    return aSB.toString ();
  }

  @Nullable
  public final CSSSourceLocation getSourceLocation ()
  {
    return m_aSourceLocation;
  }

  public final void setSourceLocation (@Nullable final CSSSourceLocation aSourceLocation)
  {
    m_aSourceLocation = aSourceLocation;
  }

  @Override
  public boolean equals (final Object o)
  {
    if (o == this)
      return true;
    if (o == null || !getClass ().equals (o.getClass ()))
      return false;
    final CSSMediaQuery rhs = (CSSMediaQuery) o;
    return m_eModifier.equals (rhs.m_eModifier) &&
           EqualsHelper.equals (m_sMedium, rhs.m_sMedium) &&
           m_aMediaExpressions.equals (rhs.m_aMediaExpressions);
  }

  @Override
  public int hashCode ()
  {
    return new HashCodeGenerator (this).append (m_eModifier).append (m_sMedium).append (m_aMediaExpressions).getHashCode ();
  }

  @Override
  public String toString ()
  {
    return new ToStringGenerator (this).append ("Modifier", m_eModifier)
                                       .append ("Medium", m_sMedium)
                                       .append ("Expressions", m_aMediaExpressions)
                                       .appendIfNotNull ("SourceLocation", m_aSourceLocation)
                                       .getToString ();
  }
}
