void InputCaptureWindow::setup() {
  window = new Window;
  window->setObjectName("input-capture-window");
  window->setWindowTitle("Input Capture");

  layout = new QVBoxLayout;
  layout->setMargin(Style::WindowMargin);
  layout->setSpacing(0);

  hlayout = new QHBoxLayout;
  hlayout->setSpacing(Style::WidgetSpacing); {
    title = new QLabel;
    hlayout->addWidget(title, 0, Qt::AlignTop);

    mouseAxes = new QPushButton("  Assign Mouse Axis  ");
    mouseAxes->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    hlayout->addWidget(mouseAxes, 0, Qt::AlignTop);

    mouseButtons = new QPushButton("  Assign Mouse Button  ");
    mouseButtons->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    hlayout->addWidget(mouseButtons, 0, Qt::AlignTop);
  }
  layout->addLayout(hlayout);

  imageSpacer = new QWidget;
  imageSpacer->setFixedSize(Style::WidgetSpacing, Style::WidgetSpacing);
  layout->addWidget(imageSpacer);

  imageWidget = new ImageWidget;
  layout->addWidget(imageWidget, 0, Qt::AlignHCenter);

  spacer = new QWidget;
  spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  layout->addWidget(spacer);

  window->setLayout(layout);
  connect(mouseAxes, SIGNAL(released()), this, SLOT(assignMouseAxis()));
  connect(mouseButtons, SIGNAL(released()), this, SLOT(assignMouseButton()));

  winInputMouseCaptureWindow = new InputMouseCaptureWindow;
  winInputMouseCaptureWindow->setup();

  winInputCalibration = new InputCalibrationWindow;
  winInputCalibration->setup();
}

void InputCaptureWindow::activate(InputObject *object) {
  if(!activeGroup) window->hide();

  utf8 info;
  info << "<b>ID:</b> ";
  if(object->parent) {
    InputDevice *device = dynamic_cast<InputDevice*>(object->parent);
    if(device) info << "Controller port " << (int)(device->port + 1) << " <b>::</b> ";
    else info << "User interface <b>::</b> ";
    info << object->parent->name << " <b>::</b> ";
  }
  info << object->name << "<br>";

  activeObject = object;
  if(activeObject->type == InputObject::Button) {
    mouseAxes->hide();
    mouseButtons->show();

    info << "Press any key or button to assign to this ID.";
  } else /*(activeObject->type == InputObject::Axis)*/ {
    mouseAxes->show();
    mouseButtons->hide();

    info << "Move any axis to assign to this ID.";
  }

  if(dynamic_cast<Joypad*>(activeObject->parent)) {
    imageSpacer->show();
    imageWidget->setFixedSize(480, 210);
    imageWidget->show();
  } else {
    imageSpacer->hide();
    imageWidget->hide();
  }

  title->setText(info);
  utility.showCentered(window);
}

void InputCaptureWindow::activate(InputGroup *group) {
  activeGroup = group;
  groupIndex = 0;
  window->hide();
  activate((*activeGroup)[groupIndex]);
}

void InputCaptureWindow::inputEvent(uint16_t code, bool forceAssign /* = false */) {
  if(!activeObject || inputLock == true) return;

  //input polling is global, need to block mouse actions that may be UI interactions.
  //custom controls on window allow mouse assignment instead.
  if(forceAssign == false) {
    if(winInputMouseCaptureWindow->window->isActiveWindow()) {
      winInputMouseCaptureWindow->inputEvent(code);
      return;
    }
    if(!window->isActiveWindow()) return;

    //get as much info as possible about this code
    InputCode::type_t type = InputCode::type(code);
    InputCode::axistype_t axisType = InputCode::axisType(code);
    int joypadNumber = InputCode::joypadNumber(code);
    int16_t state = inputManager.state(code);
    unsigned distance = abs(state - inputManager.lastState(code));

    if(type == InputCode::JoypadHat) {
      //hats can be in any of nine clock-wise states (4x direct, 4x angled and centered.)
      //only map when hat is moved to an explicit direction.
      if(state != joypad<>::hat_up   && state != joypad<>::hat_down
      && state != joypad<>::hat_left && state != joypad<>::hat_right) return;
    }

    if(type == InputCode::JoypadAxis) {
      //require a degree of resistance to prevent accidental mapping
      if(axisType == InputCode::Stick) {
        //require axis to be pressed almost completely toward a specific direction
        if(state > -28672 && state < +28672) return;
      } else if(axisType == InputCode::Trigger) {
        //require trigger to be at least 75% pressed down
        if(state > -16384) return;
      } else {
        //invalid axis type: most likely the controller has yet to be calibrated
        if(joypadNumber < 0) return;  //should never occur

        //some analog triggers report phantom motion even when user does not touch gamepad
        if(distance < 64) return;  //require some degree of force to trigger calibration
        if(state > -8192 && state < +8192) return;  //ignore center motion

        if(inputManager.calibrated(joypadNumber) == false) {
          winInputCalibration->activate(joypadNumber);
        }

        //block assignment until controller is fully calibrated
        return;
      }
    }

    if(activeObject->type == InputObject::Axis) {
      if(type == InputCode::KeyboardButton
      || type == InputCode::MouseAxis
      || type == InputCode::MouseButton
      || type == InputCode::JoypadHat
      || type == InputCode::JoypadButton
      ) return;

      //uni-directional trigger cannot map to bi-directional axis
      if(type == InputCode::JoypadAxis && axisType == InputCode::Trigger) return;
    }

    if(activeObject->type == InputObject::Button) {
      if(type == InputCode::MouseAxis
      || type == InputCode::MouseButton
      ) return;

      //only capture on button press, not release
      if(type != InputCode::JoypadAxis && state == false) return;
    }

    //if assigning a complete controller set, ensure requested key has not been assigned
    //to a previous entry in the group already. this prevents slow motion of joypad axes
    //from assigning the same value to multiple entries in rapid succession.
    if(activeGroup && groupIndex > 0) {
      for(unsigned i = 0; i < groupIndex; i++) {
        if(code == (*activeGroup)[i]->code) {
          //joypad hats and axes have multiple states, and are differentiated by modifier.
          //allow mapping only if requested modifier is unique.
          if(type == InputCode::JoypadHat) {
            if(state == joypad<>::hat_up    && (*activeGroup)[i]->modifier == InputObject::Up   ) return;
            if(state == joypad<>::hat_down  && (*activeGroup)[i]->modifier == InputObject::Down ) return;
            if(state == joypad<>::hat_left  && (*activeGroup)[i]->modifier == InputObject::Left ) return;
            if(state == joypad<>::hat_right && (*activeGroup)[i]->modifier == InputObject::Right) return;
          } else if(type == InputCode::JoypadAxis) {
            if(axisType == InputCode::Stick) {
              if(state <  0 && (*activeGroup)[i]->modifier == InputObject::Lo) return;
              if(state >= 0 && (*activeGroup)[i]->modifier == InputObject::Hi) return;
            } else if(axisType == InputCode::Trigger) {
              if((*activeGroup)[i]->modifier == InputObject::Trigger) return;
            }
          } else {
            //this code has already been used, do not map it
            return;
          }
        }
      }
    }
  }

  //bind code and update GUI input assignment list
  activeObject->bind(code);
  winInputSettings->updateList();
  activeObject = 0;

  //ignore multiple simultaneous state changes.
  //this helps with joypads that only activate
  //analog inputs after the first button press.
  inputLock = true;
  for(unsigned i = 0; i < 2; i++) inputManager.refresh();
  inputLock = false;

  if(!activeGroup) {
    window->hide();
    winInputMouseCaptureWindow->window->hide();
  } else {
    //try and map the next code in this input group
    groupIndex++;
    if(groupIndex < activeGroup->size()) {
      activate((*activeGroup)[groupIndex]);
    } else {
      //all group codes mapped
      window->hide();
      winInputMouseCaptureWindow->window->hide();
      activeGroup = 0;
    }
  }
}

void InputCaptureWindow::assignMouseAxis() {
  //refresh input state so that mouse release event (from SIGNAL(released())
  //is not sent immediately after window is visible.
  inputManager.refresh();
  winInputMouseCaptureWindow->activate(InputMouseCaptureWindow::AxisMode);
}

void InputCaptureWindow::assignMouseButton() {
  inputManager.refresh();
  winInputMouseCaptureWindow->activate(InputMouseCaptureWindow::ButtonMode);
}

InputCaptureWindow::InputCaptureWindow() {
  activeObject = 0;
  activeGroup = 0;
  groupIndex = 0;
  inputLock = false;
}

void InputCaptureWindow::Window::closeEvent(QCloseEvent*) {
  //window closed by user, cancel key assignment
  winInputCapture->activeObject = 0;
  winInputCapture->activeGroup = 0;
}

void InputCaptureWindow::ImageWidget::paintEvent(QPaintEvent*) {
  //currently, there is only an image available for the joypad.
  //in the future, this routine should determine which type of
  //image to draw via activeObject->parent's derived class type.
  QPainter painter(this);
  QPixmap pixmap(":/joypad.png");
  painter.drawPixmap(0, 0, pixmap);
}

//=======================
//InputMouseCaptureWindow
//=======================

void InputMouseCaptureWindow::setup() {
  window = new QWidget;
  window->setObjectName("input-mouse-capture-window");
  window->setWindowTitle("Mouse Input Capture");

  layout = new QVBoxLayout;
  layout->setMargin(Style::WindowMargin);
  layout->setSpacing(0);

  info = new QLabel;
  layout->addWidget(info);
  layout->addSpacing(Style::WidgetSpacing);

  captureBox = new QLabel("[ capture box ]");
  captureBox->setObjectName("mouse-capture-box");
  captureBox->setAlignment(Qt::AlignCenter);
  captureBox->setFixedHeight(120);
  layout->addWidget(captureBox);

  buttonLayout = new QHBoxLayout;
  buttonLayout->setSpacing(Style::WidgetSpacing); {
    xAxis = new QPushButton("X-axis");
    buttonLayout->addWidget(xAxis);

    yAxis = new QPushButton("Y-axis");
    buttonLayout->addWidget(yAxis);
  }
  layout->addLayout(buttonLayout);

  spacer = new QWidget;
  spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  layout->addWidget(spacer);

  window->setLayout(layout);
  connect(xAxis, SIGNAL(released()), this, SLOT(assignAxisX()));
  connect(yAxis, SIGNAL(released()), this, SLOT(assignAxisY()));
}

void InputMouseCaptureWindow::activate(InputMouseCaptureWindow::Mode mode_) {
  window->hide();
  activeMode = mode_;

  if(activeMode == AxisMode) {
    captureBox->hide();
    xAxis->show();
    yAxis->show();

    info->setText(utf8()
    << "To assign a mouse axis to this ID, please click the desired axis button below,<br>"
    << "using the mouse that you want the axis to be assigned to."
    );

    activeMouse = -1;
  } else /*(activeMode == ButtonMode) */ {
    captureBox->show();
    xAxis->hide();
    yAxis->hide();

    info->setText(utf8()
    << "To assign a mouse button to this ID, please place the mouse you wish to map<br>"
    << "over the capture box below, and then click any mouse button to set assignment."
    );
  }

  utility.showCentered(window);
}

//this is only called when isActiveWindow() == true
void InputMouseCaptureWindow::inputEvent(uint16_t code) {
  InputCode::type_t type = InputCode::type(code);
  int16_t state = inputManager.state(code);

  if(activeMode == AxisMode) {
    //when pressing down mouse button (eg to select "X-axis" or "Y-axis"),
    //record mouse index for later assignment
    if(type == InputCode::MouseButton && state == true) {
      activeMouse = InputCode::mouseNumber(code);
      return;
    }
  } else if(activeMode == ButtonMode) {
    //if this is a mouse button that is being released ...
    if(type == InputCode::MouseButton && state == false) {
      //ensure button was clicked inside active capture box
      QRect windowRect = window->geometry();
      QRect widgetRect = captureBox->geometry();
      unsigned wx = windowRect.left() + widgetRect.left();
      unsigned wy = windowRect.top() + widgetRect.top();
      unsigned px = QCursor::pos().x();
      unsigned py = QCursor::pos().y();

      if(px < wx || px >= wx + widgetRect.size().width() ) return;
      if(py < wy || py >= wy + widgetRect.size().height()) return;

      winInputCapture->inputEvent(code, true);
      return;
    }
  }
}

void InputMouseCaptureWindow::assignAxisX() {
  if(activeMouse >= 0) {
    winInputCapture->inputEvent(mouse<>::index(activeMouse, mouse<>::x), true);
  }
}

void InputMouseCaptureWindow::assignAxisY() {
  if(activeMouse >= 0) {
    winInputCapture->inputEvent(mouse<>::index(activeMouse, mouse<>::y), true);
  }
}

//====================
//InputCalibrateWindow
//====================

//background:
//===========
//HID devices work by sending device state *changes* only. what this means is that when an application is started,
//it does not know the current state of said device. the keyboard and mouse are exceptions, as the OS globally
//tracks their states. but this does apply to joypads. once a button is pressed, or an axis is moved, the entire
//joypad state will be sent in a message, that APIs such as DirectInput and SDL will buffer.
//
//to complicate matters, recent joypads have added pressure-sensitive buttons (triggers), in addition to the
//existing analog sticks. but this functionality was not extended to the USB HID state or to various platform
//input APIs. instead, they are treated exactly like axes.
//
//however, an application needs to treat these two input types distinctly:
//a stick is a bi-directional input. the stick starts off centered, and can be moved left or right, or up or down.
//a trigger is a uni-directional input. it can only be pushed down.
//
//a stick's default, unpressed state is centered (0), whereas a trigger's default state is fully depressed (+32767.)
//but because the default state is not available until the user presses a button on a joypad, it is not possible to
//calibrate a joypad on startup. all axes will report a value of 0, even if buttons are depressed.
//
//thusly, this class is needed. it will spawn a window upon the first attempt to map a joypad axis after startup.
//by the point this window appears, an axis must have been moved, so the joypad state is now valid. but since it's
//not possible to tell which button was pressed or which axis was moved, it's possible the axis that we're trying to
//map was moved. so querying its state now might result in improper mapping. so instead, this window is shown, and
//the user is asked not to press any buttons or move any axes. after hitting okay to confirm the joypad is idle,
//the joypad can finally be calibrated properly.
//
//upon assignment, the calibration data is appended to the input assignment value (eg "joypad00.axis00::trigger"),
//so that calibration is not necessary every time the emulator is run -- only when modifying input mapping on an axis.

void InputCalibrationWindow::activate(unsigned joy) {
  //do not override an already active calibration window
  if(window->isVisible()) return;

  activeJoypad = joy;
  info->setText(utf8()
  << "Joypad #" << joy << " needs to be calibrated before it can be mapped. "
  << "Please ensure that<br>no buttons are pressed, "
  << "and all axes are centered before pressing okay."
  );

  utility.showCentered(window);
  ok->setFocus();
}

void InputCalibrationWindow::setup() {
  window = new Window;
  window->setObjectName("input-calibrate-window");
  window->setWindowTitle("Joypad Calibration");

  layout = new QVBoxLayout;
  layout->setMargin(Style::WindowMargin);
  layout->setSpacing(0);

  info = new QLabel;
  layout->addWidget(info);
  layout->addSpacing(Style::WidgetSpacing);

  ok = new QPushButton("Ok");
  layout->addWidget(ok);

  spacer = new QWidget;
  spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  layout->addWidget(spacer);

  window->setLayout(layout);
  connect(ok, SIGNAL(released()), this, SLOT(dismiss()));
}

void InputCalibrationWindow::dismiss() {
  window->hide();
  if(activeJoypad != -1) {
    inputManager.calibrate(activeJoypad);
    activeJoypad = -1;
  }
}

void InputCalibrationWindow::Window::closeEvent(QCloseEvent*) {
  winInputCalibration->dismiss();
}
