/*
 * Copyright (c) 2003-2009 Sun Microsystems, Inc. All Rights Reserved.
 * Copyright (c) 2010 JogAmp Community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * - Redistribution of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistribution in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * Neither the name of Sun Microsystems, Inc. or the names of
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
 * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
 * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
 * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
 * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
 * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
 * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed or intended for use
 * in the design, construction, operation or maintenance of any nuclear
 * facility.
 *
 * Sun gratefully acknowledges that this software was originally authored
 * and developed by Kenneth Bradley Russell and Christopher John Kline.
 */

package com.jogamp.opengl;

import com.jogamp.nativewindow.NativeWindowException;

import java.util.List;

import com.jogamp.nativewindow.CapabilitiesImmutable;

import com.jogamp.common.ExceptionUtils;
import com.jogamp.common.util.PropertyAccess;

import jogamp.opengl.Debug;

/** <P> The default implementation of the {@link
    GLCapabilitiesChooser} interface, which provides consistent visual
    selection behavior across platforms. The precise algorithm is
    deliberately left loosely specified. Some properties are: </P>

    <UL>

    <LI> As long as there is at least one available non-null
    GLCapabilities which matches the "stereo" option, will return a
    valid index.

    <LI> Attempts to match as closely as possible the given
    GLCapabilities, but will select one with fewer capabilities (i.e.,
    lower color depth) if necessary.

    <LI> Prefers hardware-accelerated visuals to
    non-hardware-accelerated.

    <LI> If there is no exact match, prefers a more-capable visual to
    a less-capable one.

    <LI> If there is more than one exact match, chooses an arbitrary
    one.

    <LI> May select the opposite of a double- or single-buffered
    visual (based on the user's request) in dire situations.

    <LI> Color depth (including alpha) mismatches are weighted higher
    than depth buffer mismatches, which are in turn weighted higher
    than accumulation buffer (including alpha) and stencil buffer
    depth mismatches.

    <LI> If a valid windowSystemRecommendedChoice parameter is
    supplied, chooses that instead of using the cross-platform code.

    </UL>
*/

public class DefaultGLCapabilitiesChooser implements GLCapabilitiesChooser {
  private static final boolean DEBUG;

  static {
      Debug.initSingleton();
      DEBUG = PropertyAccess.isPropertyDefined("jogl.debug.CapabilitiesChooser", true);
  }

  private final static int NO_SCORE = -9999999;
  private final static int DOUBLE_BUFFER_MISMATCH_PENALTY = 1000;
  private final static int OPAQUE_MISMATCH_PENALTY = 750;
  private final static int STENCIL_MISMATCH_PENALTY = 500;
  private final static int MULTISAMPLE_MISMATCH_PENALTY = 500;
  private final static int MULTISAMPLE_EXTENSION_MISMATCH_PENALTY = 250; // just a little drop, no scale
  // Pseudo attempt to keep equal rank penalties scale-equivalent
  // (e.g., stencil mismatch is 3 * accum because there are 3 accum
  // components)
  private final static int COLOR_MISMATCH_PENALTY_SCALE     = 36;
  private final static int DEPTH_MISMATCH_PENALTY_SCALE     = 6;
  private final static int ACCUM_MISMATCH_PENALTY_SCALE     = 1;
  private final static int STENCIL_MISMATCH_PENALTY_SCALE   = 3;
  private final static int MULTISAMPLE_MISMATCH_PENALTY_SCALE   = 3;

  @Override
  public int chooseCapabilities(final CapabilitiesImmutable desired,
                                final List<? extends CapabilitiesImmutable> available,
                                final int windowSystemRecommendedChoice) {
    if ( null == desired ) {
      throw new NativeWindowException("Null desired capabilities");
    }
    if ( 0 == available.size() ) {
      throw new NativeWindowException("Empty available capabilities");
    }

    final GLCapabilitiesImmutable gldes = (GLCapabilitiesImmutable) desired;
    final int availnum = available.size();

    if (DEBUG) {
      ExceptionUtils.dumpStack(System.err);
      System.err.println("Desired: " + gldes);
      System.err.println("Available: " + availnum);
      for (int i = 0; i < available.size(); i++) {
        System.err.println(i + ": " + available.get(i));
      }
      System.err.println("Window system's recommended choice: " + windowSystemRecommendedChoice);
    }

    if (windowSystemRecommendedChoice >= 0 &&
        windowSystemRecommendedChoice < availnum &&
        null != available.get(windowSystemRecommendedChoice)) {
      if (DEBUG) {
        System.err.println("Choosing window system's recommended choice of " + windowSystemRecommendedChoice);
        System.err.println(available.get(windowSystemRecommendedChoice));
      }
      return windowSystemRecommendedChoice;
    }

    // Create score array
    final int[] scores = new int[availnum];

    for (int i = 0; i < scores.length; i++) {
      scores[i] = NO_SCORE;
    }
    final int gldes_samples = gldes.getNumSamples();

    // Compute score for each
    for (int i = 0; i < availnum; i++) {
      final GLCapabilitiesImmutable cur = (GLCapabilitiesImmutable) available.get(i);
      if (cur == null) {
        continue;
      }
      if (gldes.isOnscreen() && !cur.isOnscreen()) {
        continue; // requested onscreen, but n/a
      }
      if (!gldes.isOnscreen()) {
          /** FBO is generic ..
          if (gldes.isFBO() && !cur.isFBO()) {
            continue; // requested FBO, but n/a
          }  */
          if (gldes.isPBuffer() && !cur.isPBuffer()) {
            continue; // requested pBuffer, but n/a
          }
          if (gldes.isBitmap() && !cur.isBitmap()) {
            continue; // requested pBuffer, but n/a
          }
      }
      if (gldes.getStereo() != cur.getStereo()) {
        continue;
      }
      final int cur_samples = cur.getNumSamples() ;
      int score = 0;

      // Compute difference in color depth
      // (Note that this decides the direction of all other penalties)
      score += (COLOR_MISMATCH_PENALTY_SCALE *
                ((cur.getRedBits() + cur.getGreenBits() + cur.getBlueBits() + cur.getAlphaBits()) -
                 (gldes.getRedBits() + gldes.getGreenBits() + gldes.getBlueBits() + gldes.getAlphaBits())));
      // Compute difference in depth buffer depth
      score += (DEPTH_MISMATCH_PENALTY_SCALE * sign(score) *
                Math.abs(cur.getDepthBits() - gldes.getDepthBits()));
      // Compute difference in accumulation buffer depth
      score += (ACCUM_MISMATCH_PENALTY_SCALE * sign(score) *
                Math.abs((cur.getAccumRedBits() + cur.getAccumGreenBits() + cur.getAccumBlueBits() + cur.getAccumAlphaBits()) -
                         (gldes.getAccumRedBits() + gldes.getAccumGreenBits() + gldes.getAccumBlueBits() + gldes.getAccumAlphaBits())));
      // Compute difference in stencil bits
      score += STENCIL_MISMATCH_PENALTY_SCALE * sign(score) * (cur.getStencilBits() - gldes.getStencilBits());
      // Compute difference in multisampling bits
      score += MULTISAMPLE_MISMATCH_PENALTY_SCALE * sign(score) * (cur_samples - gldes_samples);
      // double buffer
      if (cur.getDoubleBuffered() != gldes.getDoubleBuffered()) {
        score += sign(score) * DOUBLE_BUFFER_MISMATCH_PENALTY;
      }
      // opaque
      if (cur.isBackgroundOpaque() != gldes.isBackgroundOpaque()) {
        score += sign(score) * OPAQUE_MISMATCH_PENALTY;
      }
      if ((gldes.getStencilBits() > 0) && (cur.getStencilBits() == 0)) {
        score += sign(score) * STENCIL_MISMATCH_PENALTY;
      }
      if (gldes_samples > 0) {
          if (cur_samples == 0) {
            score += sign(score) * MULTISAMPLE_MISMATCH_PENALTY;
          }
          if (!gldes.getSampleExtension().equals(cur.getSampleExtension())) {
            score += sign(score) * MULTISAMPLE_EXTENSION_MISMATCH_PENALTY;
          }
      }
      scores[i] = score;
    }
    // Now prefer hardware-accelerated visuals by pushing scores of
    // non-hardware-accelerated visuals out
    boolean gotHW = false;
    int maxAbsoluteHWScore = 0;
    for (int i = 0; i < availnum; i++) {
      final int score = scores[i];
      if (score == NO_SCORE) {
        continue;
      }
      final GLCapabilitiesImmutable cur = (GLCapabilitiesImmutable) available.get(i);
      if (cur.getHardwareAccelerated()) {
        final int absScore = Math.abs(score);
        if (!gotHW ||
            (absScore > maxAbsoluteHWScore)) {
          gotHW = true;
          maxAbsoluteHWScore = absScore;
        }
      }
    }
    if (gotHW) {
      for (int i = 0; i < availnum; i++) {
        int score = scores[i];
        if (score == NO_SCORE) {
          continue;
        }
        final GLCapabilitiesImmutable cur = (GLCapabilitiesImmutable) available.get(i);
        if (!cur.getHardwareAccelerated()) {
          if (score <= 0) {
            score -= maxAbsoluteHWScore;
          } else if (score > 0) {
            score += maxAbsoluteHWScore;
          }
          scores[i] = score;
        }
      }
    }

    if (DEBUG) {
      System.err.print("Scores: [");
      for (int i = 0; i < availnum; i++) {
        if (i > 0) {
          System.err.print(",");
        }
        System.err.print(" " + i +": " + scores[i]);
      }
      System.err.println(" ]");
    }

    // Ready to select. Choose score closest to 0.
    int scoreClosestToZero = NO_SCORE;
    int chosenIndex = -1;
    for (int i = 0; i < availnum; i++) {
      final int score = scores[i];
      if (score == NO_SCORE) {
        continue;
      }
      // Don't substitute a positive score for a smaller negative score
      if ((scoreClosestToZero == NO_SCORE) ||
          (Math.abs(score) < Math.abs(scoreClosestToZero) &&
       ((sign(scoreClosestToZero) < 0) || (sign(score) > 0)))) {
        scoreClosestToZero = score;
        chosenIndex = i;
      }
    }
    if (chosenIndex < 0) {
      throw new NativeWindowException("Unable to select one of the provided GLCapabilities");
    }
    if (DEBUG) {
      System.err.println("Chosen index: " + chosenIndex);
      System.err.println("Chosen capabilities:");
      System.err.println(available.get(chosenIndex));
    }

    return chosenIndex;
  }

  private static int sign(final int score) {
    if (score < 0) {
      return -1;
    }
    return 1;
  }

}