/* $NetBSD: ppbus_base.c,v 1.23 2023/02/13 22:44:52 andvar Exp $ */

/*-
* Copyright (c) 1997, 1998, 1999 Nicolas Souchu
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions 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.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* FreeBSD: src/sys/dev/ppbus/ppb_base.c,v 1.10.2.1 2000/08/01 23:26:26 n_hibma Exp
*
*/

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: ppbus_base.c,v 1.23 2023/02/13 22:44:52 andvar Exp $");

#include "opt_ppbus_1284.h"
#include "opt_ppbus.h"

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/proc.h>
#include <sys/systm.h>

#include <dev/ppbus/ppbus_1284.h>
#include <dev/ppbus/ppbus_conf.h>
#include <dev/ppbus/ppbus_base.h>
#include <dev/ppbus/ppbus_device.h>
#include <dev/ppbus/ppbus_io.h>
#include <dev/ppbus/ppbus_var.h>

#ifndef DONTPROBE_1284
/* Utility functions */
static char * search_token(char *, int, const char *);
#endif

/* Perform general ppbus I/O request */
int
ppbus_io(device_t dev, int iop, u_char * addr, int cnt, u_char byte)
{
       struct ppbus_softc * bus = device_private(dev);
       return (bus->ppbus_io(device_parent(dev), iop, addr, cnt, byte));
}

/* Execute microsequence */
int
ppbus_exec_microseq(device_t dev, struct ppbus_microseq ** sequence)
{
       struct ppbus_softc * bus = device_private(dev);
       return (bus->ppbus_exec_microseq(device_parent(dev), sequence));
}

/* Read instance variables of ppbus */
int
ppbus_read_ivar(device_t dev, int index, unsigned int * val)

{
       struct ppbus_softc * bus = device_private(dev);

       switch (index) {
       case PPBUS_IVAR_INTR:
       case PPBUS_IVAR_EPP_PROTO:
       case PPBUS_IVAR_DMA:
               return (bus->ppbus_read_ivar(device_parent(dev), index, val));

       case PPBUS_IVAR_IEEE:
               *val = (bus->sc_use_ieee == PPBUS_ENABLE_IEEE) ? 1 : 0;
               break;

       default:
               return (ENOENT);
       }

       return 0;
}

/* Write an instance variable */
int
ppbus_write_ivar(device_t dev, int index, unsigned int * val)
{
       struct ppbus_softc * bus = device_private(dev);

       switch (index) {
       case PPBUS_IVAR_INTR:
       case PPBUS_IVAR_EPP_PROTO:
       case PPBUS_IVAR_DMA:
               return (bus->ppbus_write_ivar(device_parent(dev), index, val));

       case PPBUS_IVAR_IEEE:
               bus->sc_use_ieee = ((*val != 0) ? PPBUS_ENABLE_IEEE :
                       PPBUS_DISABLE_IEEE);
               break;

       default:
               return (ENOENT);
       }

       return 0;
}

/* Polls the bus for a max of 10-milliseconds */
int
ppbus_poll_bus(device_t dev, int maxp, char mask, char status,
       int how)
{
       int i, j, error;
       char r;

       /* try at least up to 10ms */
       for (j = 0; j < ((how & PPBUS_POLL) ? maxp : 1); j++) {
               for (i = 0; i < 10000; i++) {
                       r = ppbus_rstr(dev);
                       DELAY(1);
                       if ((r & mask) == status)
                               return (0);
               }
       }

       if (!(how & PPBUS_POLL)) {
          for (i = 0; maxp == PPBUS_FOREVER || i < maxp-1; i++) {
               if ((ppbus_rstr(dev) & mask) == status)
                       return (0);

               switch (how) {
               case PPBUS_NOINTR:
                       /* wait 10 ms */
                       kpause("ppbuspoll", false, hz / 100, NULL);
                       break;

               case PPBUS_INTR:
               default:
                       /* wait 10 ms */
                       error = kpause("ppbuspoll", true, hz / 100, NULL);
                       if (error != EWOULDBLOCK) {
                               return error;
                       }
                       break;
               }
          }
       }

       return (EWOULDBLOCK);
}

/* Get operating mode of the chipset */
int
ppbus_get_mode(device_t dev)
{
       struct ppbus_softc * bus = device_private(dev);

       return (bus->sc_mode);
}

/* Set the operating mode of the chipset, return 0 on success. */
int
ppbus_set_mode(device_t dev, int mode, int options)
{
       struct ppbus_softc * bus = device_private(dev);
       int error = 0;

       /* If no mode change, do nothing */
       if(bus->sc_mode == mode)
               return error;

       /* Do necessary negotiations */
       if(bus->sc_use_ieee == PPBUS_ENABLE_IEEE) {
               /* Cannot negotiate standard mode */
               if(!(mode & (PPBUS_FAST | PPBUS_COMPATIBLE))) {
                       error = ppbus_1284_negotiate(dev, mode, options);
               }
               /* Termination is unnecessary for standard<->fast */
               else if(!(bus->sc_mode & (PPBUS_FAST | PPBUS_COMPATIBLE))) {
                       ppbus_1284_terminate(dev);
               }
       }

       if(!error) {
               /* Set mode and update mode of ppbus to actual mode */
               error = bus->ppbus_setmode(device_parent(dev), mode);
               bus->sc_mode = bus->ppbus_getmode(device_parent(dev));
       }

       /* Update host state if necessary */
       if(!(error) && (bus->sc_use_ieee == PPBUS_ENABLE_IEEE)) {
               switch(mode) {
               case PPBUS_COMPATIBLE:
               case PPBUS_FAST:
               case PPBUS_EPP:
               case PPBUS_ECP:
                       ppbus_1284_set_state(dev, PPBUS_FORWARD_IDLE);
                       break;

               case PPBUS_NIBBLE:
               case PPBUS_PS2:
                       ppbus_1284_set_state(dev, PPBUS_REVERSE_IDLE);
                       break;
               }
       }

       return error;
}

/* Write characters to the port */
int
ppbus_write(device_t dev, char * buf, int len, int how, size_t * cnt)
{
       struct ppbus_softc * bus = device_private(dev);

       if(bus->sc_use_ieee == PPBUS_ENABLE_IEEE) {
               if(bus->sc_1284_state != PPBUS_FORWARD_IDLE) {
                       printf("%s(%s): bus not in forward idle mode.\n",
                               __func__, device_xname(dev));
                       return ENODEV;
               }
       }

       return (bus->ppbus_write(device_parent(bus->sc_dev), buf, len, how, cnt));
}

/* Read characters from the port */
int
ppbus_read(device_t dev, char * buf, int len, int how, size_t * cnt)
{
       struct ppbus_softc * bus = device_private(dev);

       if(bus->sc_use_ieee == PPBUS_ENABLE_IEEE) {
               if(bus->sc_1284_state != PPBUS_REVERSE_IDLE) {
                       printf("%s(%s): bus not in reverse idle mode.\n",
                               __func__, device_xname(dev));
                       return ENODEV;
               }
       }

       return (bus->ppbus_read(device_parent(dev), buf, len, how, cnt));
}

/* Reset the EPP timeout bit in the status register */
int
ppbus_reset_epp_timeout(device_t dev)
{
       struct ppbus_softc * bus = device_private(dev);

       if(bus->sc_capabilities & PPBUS_HAS_EPP) {
               bus->ppbus_reset_epp_timeout(device_parent(dev));
               return 0;
       }
       else {
               return ENODEV;
       }
}

/* Wait for the ECP FIFO to be empty */
int
ppbus_ecp_sync(device_t dev)
{
       struct ppbus_softc * bus = device_private(dev);

       if(bus->sc_capabilities & PPBUS_HAS_ECP) {
               bus->ppbus_ecp_sync(device_parent(dev));
               return 0;
       }
       else {
               return ENODEV;
       }
}

/* Allocate DMA for use with ppbus */
int
ppbus_dma_malloc(device_t dev, void ** buf, bus_addr_t * addr,
       bus_size_t size)
{
       struct ppbus_softc * ppbus = device_private(dev);

       if(ppbus->sc_capabilities & PPBUS_HAS_DMA)
               return (ppbus->ppbus_dma_malloc(device_parent(dev), buf, addr,
                       size));
       else
               return ENODEV;
}

/* Free memory allocated with ppbus_dma_malloc() */
int
ppbus_dma_free(device_t dev, void ** buf, bus_addr_t * addr,
       bus_size_t size)
{
       struct ppbus_softc * ppbus = device_private(dev);

       if(ppbus->sc_capabilities & PPBUS_HAS_DMA) {
               ppbus->ppbus_dma_free(device_parent(dev), buf, addr, size);
               return 0;
       }
       else {
               return ENODEV;
       }
}

/* Install a handler to be called by hardware interrupt handler */
int ppbus_add_handler(device_t dev, void (*func)(void *), void *arg)
{
       struct ppbus_softc * bus = device_private(dev);

       if(bus->sc_capabilities & PPBUS_HAS_INTR)
               return bus->ppbus_add_handler(device_parent(dev), func, arg);
       else
               return ENODEV;
}

/* Remove a handler registered with ppbus_add_handler() */
int ppbus_remove_handler(device_t dev, void (*func)(void *))
{
       struct ppbus_softc * bus = device_private(dev);

       if(bus->sc_capabilities & PPBUS_HAS_INTR)
               return bus->ppbus_remove_handler(device_parent(dev), func);
       else
               return ENODEV;
}

/*
* ppbus_get_status()
*
* Read the status register and update the status info
*/
int
ppbus_get_status(device_t dev, struct ppbus_status * status)
{
       register char r = status->status = ppbus_rstr(dev);

       status->timeout = r & TIMEOUT;
       status->error   = !(r & nFAULT);
       status->select  = r & SELECT;
       status->paper_end = r & PERROR;
       status->ack     = !(r & nACK);
       status->busy    = !(r & nBUSY);

       return (0);
}

/* Allocate the device to perform transfers */
int
ppbus_request_bus(device_t dev, device_t busdev, int how,
       unsigned int timeout)
{
       struct ppbus_softc * bus = device_private(dev);
       unsigned int counter = timeout;
       bool intr = (how & PPBUS_INTR) != 0;
       int error;

       /* Loop until lock acquired (if PPBUS_WAIT) or an error occurs */
       for(;;) {
               if (mutex_tryenter(&(bus->sc_lock)))
                       break;

               if(how & PPBUS_WAIT) {
                       error = kpause("ppbusreq", intr, hz / 2, NULL);
                       counter -= (hz/2);
                       if(!(error))
                               continue;
                       else if(error != EWOULDBLOCK)
                               goto end;
                       if(counter == 0) {
                               error = ETIMEDOUT;
                               goto end;
                       }
               }
               else {
                       error = EWOULDBLOCK;
                       goto end;
               }
       }

       /* Set bus owner or return error if bus is taken */
       if(bus->ppbus_owner == NULL) {
               bus->ppbus_owner = busdev;
               error = 0;
       }
       else {
               error = EBUSY;
       }

       /* Release lock */
       mutex_exit(&(bus->sc_lock));

end:
       return error;
}

/* Release the device allocated with ppbus_request_bus() */
int
ppbus_release_bus(device_t dev, device_t busdev, int how,
       unsigned int timeout)
{
       struct ppbus_softc * bus = device_private(dev);
       unsigned int counter = timeout;
       bool intr = (how & PPBUS_INTR) != 0;
       int error;

       /* Loop until lock acquired (if PPBUS_WAIT) or an error occurs */
       for(;;) {
               if (mutex_tryenter(&(bus->sc_lock)))
                       break;

               if(how & PPBUS_WAIT) {
                       error = kpause("ppbusrel", intr, hz / 2, NULL);
                       counter -= (hz/2);
                       if(!(error))
                               continue;
                       else if(error != EWOULDBLOCK)
                               goto end;
                       if(counter == 0) {
                               error = ETIMEDOUT;
                               goto end;
                       }
               }
               else {
                       error = EWOULDBLOCK;
                       goto end;
               }
       }

       /* If the device is the owner, release bus */
       if(bus->ppbus_owner != busdev) {
               error = EINVAL;
       }
       else {
               bus->ppbus_owner = NULL;
               error = 0;
       }

       /* Release lock */
       mutex_exit(&(bus->sc_lock));

end:
       return error;
}


/* IEEE 1284-based probes */

#ifndef DONTPROBE_1284

static const char *pnp_tokens[] = {
       "PRINTER", "MODEM", "NET", "HDC", "PCMCIA", "MEDIA",
       "FDC", "PORTS", "SCANNER", "DIGICAM", "", NULL };

/* ??? */
#if 0
static char *pnp_classes[] = {
       "printer", "modem", "network device",
       "hard disk", "PCMCIA", "multimedia device",
       "floppy disk", "ports", "scanner",
       "digital camera", "unknown device", NULL };
#endif

/*
* Search the first occurrence of a token within a string
* XXX should use strxxx() calls
*/
static char *
search_token(char *str, int slen, const char *token)
{
       const char *p;
       int tlen, i, j;

#define UNKNOWN_LENGTH  -1

       if (slen == UNKNOWN_LENGTH)
              /* get string's length */
              for (slen = 0, p = str; *p != '\0'; p++)
                      slen++;

      /* get token's length */
      for (tlen = 0, p = token; *p != '\0'; p++)
              tlen++;

      if (tlen == 0)
              return (str);

      for (i = 0; i <= slen-tlen; i++) {
              for (j = 0; j < tlen; j++)
                      if (str[i+j] != token[j])
                              break;
              if (j == tlen)
                      return (&str[i]);
      }

       return (NULL);
}

/* Stores the class ID of the peripheral in soft config data */
void
ppbus_pnp_detect(device_t dev)
{
       struct ppbus_softc * bus = device_private(dev);
       int i;
       int error;
       size_t len = 0;
       size_t str_sz = 0;
       char * str = NULL;
       char * class = NULL;
       char * token;

#ifdef PPBUS_VERBOSE
       printf("%s: Probing for PnP devices.\n", device_xname(dev));
#endif

       error = ppbus_1284_read_id(dev, PPBUS_NIBBLE, &str, &str_sz, &len);
       if(str_sz != len) {
#ifdef DEBUG_1284
               printf("%s(%s): device returned less characters than expected "
                       "in device ID.\n", __func__, device_xname(dev));
#endif
       }
       if(error) {
               printf("%s: Error getting device ID (errno = %d)\n",
                       device_xname(dev), error);
               goto end_detect;
       }

#ifdef DEBUG_1284
       printf("%s: <PnP> %zu characters: ", device_xname(dev), len);
       for (i = 0; i < len; i++)
               printf("%c(0x%x) ", str[i], str[i]);
       printf("\n");
#endif

       /* replace ';' characters by '\0' */
       for (i = 0; i < len; i++)
               if(str[i] == ';') str[i] = '\0';
               /* str[i] = (str[i] == ';') ? '\0' : str[i]; */

       if ((token = search_token(str, len, "MFG")) != NULL ||
               (token = search_token(str, len, "MANUFACTURER")) != NULL)
               printf("%s: <%s", device_xname(dev),
                       search_token(token, UNKNOWN_LENGTH, ":") + 1);
       else
               printf("%s: <unknown", device_xname(dev));

       if ((token = search_token(str, len, "MDL")) != NULL ||
               (token = search_token(str, len, "MODEL")) != NULL)
               printf(" %s",
                       search_token(token, UNKNOWN_LENGTH, ":") + 1);

       if ((token = search_token(str, len, "REV")) != NULL)
               printf(".%s",
                       search_token(token, UNKNOWN_LENGTH, ":") + 1);

       printf(">");

       if ((token = search_token(str, len, "CLS")) != NULL) {
               class = search_token(token, UNKNOWN_LENGTH, ":") + 1;
               printf(" %s", class);
       }

       if ((token = search_token(str, len, "CMD")) != NULL ||
               (token = search_token(str, len, "COMMAND")) != NULL)
               printf(" %s",
                       search_token(token, UNKNOWN_LENGTH, ":") + 1);

       printf("\n");

       if (class) {
               /* identify class ident */
               for (i = 0; pnp_tokens[i] != NULL; i++) {
                       if (search_token(class, len, pnp_tokens[i]) != NULL) {
                               bus->sc_class_id = i;
                               goto end_detect;
                       }
               }
       }
       bus->sc_class_id = PPBUS_PNP_UNKNOWN;

end_detect:
       if(str)
               free(str, M_DEVBUF);
       return;
}

/* Scan the ppbus for IEEE1284 compliant devices */
int
ppbus_scan_bus(device_t dev)
{
       struct ppbus_softc * bus = device_private(dev);
       int error;

       /* Try IEEE1284 modes, one device only (no IEEE1284.3 support) */

       error = ppbus_1284_negotiate(dev, PPBUS_NIBBLE, 0);
       if (error && bus->sc_1284_state == PPBUS_ERROR
           && bus->sc_1284_error == PPBUS_NOT_IEEE1284)
               return (error);
       ppbus_1284_terminate(dev);

#if defined(PPBUS_VERBOSE) || defined(PPBUS_DEBUG)
       /* IEEE1284 supported, print info */
       printf("%s: IEEE1284 negotiation: modes %s",
           device_xname(dev), "NIBBLE");

       error = ppbus_1284_negotiate(dev, PPBUS_PS2, 0);
       if (!error)
               printf("/PS2");
       ppbus_1284_terminate(dev);

       error = ppbus_1284_negotiate(dev, PPBUS_ECP, 0);
       if (!error)
               printf("/ECP");
       ppbus_1284_terminate(dev);

       error = ppbus_1284_negotiate(dev, PPBUS_ECP, PPBUS_USE_RLE);
       if (!error)
               printf("/ECP_RLE");
       ppbus_1284_terminate(dev);

       error = ppbus_1284_negotiate(dev, PPBUS_EPP, 0);
       if (!error)
               printf("/EPP");
       ppbus_1284_terminate(dev);

       printf("\n");
#endif /* PPBUS_VERBOSE || PPBUS_DEBUG */

       return 0;
}

#endif /* !DONTPROBE_1284 */