/**
 * exynos_drd_switch_set_host -  bind/unbind the host controller driver.
 *
 * @otg: Pointer to the usb_otg structure.
 * @host: Pointer to the usb_bus structure.
 *
 * Returns 0 on success otherwise negative errno.
 */
static int exynos_drd_switch_set_host(struct usb_otg *otg, struct usb_bus *host)
{
	struct exynos_drd_switch *drd_switch = container_of(otg,
					struct exynos_drd_switch, otg);
	bool activate = false;
	unsigned long flags;

	spin_lock_irqsave(&drd_switch->lock, flags);

	if (host) {
		dev_dbg(otg->phy->dev, "Binding host %s\n", host->bus_name);
		otg->host = host;

		/*
		 * Prevents unnecessary activation of the work function.
		 * If both peripheral and host are set or if ID pin is low
		 * then we ensure that work function will enter to valid state.
		 */
		if (otg->gadget || drd_switch->id_state == A_DEV)
			activate = true;
	} else {
		dev_dbg(otg->phy->dev, "Unbinding host\n");

		if (otg->phy->state == OTG_STATE_A_HOST) {
			exynos_drd_switch_start_host(otg, 0);
			otg->host = NULL;
			otg->phy->state = OTG_STATE_UNDEFINED;
			activate = true;
		} else {
			otg->host = NULL;
		}
	}

	spin_unlock_irqrestore(&drd_switch->lock, flags);

	if (activate)
		exynos_drd_switch_schedule_work(&drd_switch->work);

	return 0;
}
/**
 * exynos_drd_switch_work - work function.
 *
 * @w: Pointer to the exynos otg work structure.
 *
 * NOTE: After any change in phy->state,
 * we must reschdule the state machine.
 */
static void exynos_drd_switch_work(struct work_struct *w)
{
    struct exynos_drd_switch *drd_switch = container_of(w,
                                           struct exynos_drd_switch, work.work);
    struct usb_phy *phy = drd_switch->otg.phy;
    struct delayed_work *work = &drd_switch->work;
    enum usb_otg_state state, new_state;
    enum id_pin_state id_state;
    bool vbus_active;
    unsigned long flags;
    int ret = 0;

    state = phy->state;
    new_state = state;

    if (atomic_read(&drd_switch->sm_reset)) {
        if (state == OTG_STATE_A_HOST)
            exynos_drd_switch_start_host(&drd_switch->otg, 0);
        else if (state == OTG_STATE_B_PERIPHERAL)
            exynos_drd_switch_start_peripheral(&drd_switch->otg, 0);

        new_state = OTG_STATE_UNDEFINED;
        goto exit;
    }

    spin_lock_irqsave(&drd_switch->lock, flags);
    id_state = drd_switch->id_state;
    vbus_active = drd_switch->vbus_active;
    spin_unlock_irqrestore(&drd_switch->lock, flags);

    /* Check OTG state */
    switch (state) {
    case OTG_STATE_UNDEFINED:
        /* Switch to A or B-Device according to ID state */
        if (id_state == B_DEV)
            new_state = OTG_STATE_B_IDLE;
        else if (id_state == A_DEV)
            new_state = OTG_STATE_A_IDLE;

        exynos_drd_switch_schedule_work(work);
        break;
    case OTG_STATE_B_IDLE:
        if (id_state == A_DEV) {
            new_state = OTG_STATE_A_IDLE;
            exynos_drd_switch_schedule_work(work);
        } else if (vbus_active) {
            /* Start peripheral only if B-Session is valid */
            ret = exynos_drd_switch_start_peripheral(
                      &drd_switch->otg, 1);
            if (!ret) {
                new_state = OTG_STATE_B_PERIPHERAL;
                exynos_drd_switch_schedule_work(work);
            } else if (ret == -EAGAIN) {
                exynos_drd_switch_schedule_dwork(work,
                                                 EAGAIN_DELAY);
            } else {
                /* Fatal error */
                dev_err(phy->dev,
                        "unable to start B-device\n");
            }
        } else {
            dev_dbg(phy->dev, "VBus is not active\n");
        }
        break;
    case OTG_STATE_B_PERIPHERAL:
        dev_dbg(phy->dev, "OTG_STATE_B_PERIPHERAL\n");
        if ((id_state == A_DEV) || !vbus_active) {
            exynos_drd_switch_start_peripheral(&drd_switch->otg, 0);
            new_state = OTG_STATE_B_IDLE;
            exynos_drd_switch_schedule_work(work);
        }
        break;
    case OTG_STATE_A_IDLE:
        if (id_state == B_DEV) {
            new_state = OTG_STATE_B_IDLE;
            exynos_drd_switch_schedule_work(work);
        } else {
            /* Switch to A-Device */
            ret = exynos_drd_switch_start_host(&drd_switch->otg, 1);
            if (!ret || ret == -EINPROGRESS) {
                new_state = OTG_STATE_A_HOST;
                exynos_drd_switch_schedule_work(work);
            } else if (ret == -EAGAIN || ret == -EBUSY) {
                dev_vdbg(phy->dev, "host turn on retry\n");
                exynos_drd_switch_schedule_dwork(work,
                                                 EAGAIN_DELAY);
            } else {
                /* Fatal error */
                dev_err(phy->dev,
                        "unable to start A-device\n");
            }
        }
        break;
    case OTG_STATE_A_HOST:
        dev_dbg(phy->dev, "OTG_STATE_A_HOST\n");
        if (id_state == B_DEV) {
            ret = exynos_drd_switch_start_host(&drd_switch->otg, 0);
            /* Currently we ignore most of the errors */
            if (ret == -EAGAIN) {
                dev_vdbg(phy->dev, "host turn off retry\n");
                exynos_drd_switch_schedule_dwork(work,
                                                 EAGAIN_DELAY);
            } else {
                if (ret == -EINVAL || ret == -EACCES)
                    /* Fatal error */
                    dev_err(phy->dev,
                            "unable to stop A-device\n");

                new_state = OTG_STATE_A_IDLE;
                exynos_drd_switch_schedule_work(work);
            }
        }
        break;
    default:
        dev_err(phy->dev, "invalid otg-state\n");

    }

exit:
    WARN((phy->state != state), "PHY state has been changed outside of SM\n");
    phy->state = new_state;
}